Introduce new batch event for life lost for a specific player (#13071)

* Introduce new batch event for life lost for a specific player

closes #12202, fix #10805

* implement [DSC] Valgavoth, Harrower of Souls

* text fixes
This commit is contained in:
xenohedron 2024-11-19 23:41:34 -05:00 committed by GitHub
parent 95e986dee7
commit d6cf207a8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 693 additions and 707 deletions

View file

@ -1,8 +1,8 @@
package mage.cards.b;
import mage.MageInt;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.dynamicvalue.common.SavedGainedLifeValue;
import mage.abilities.common.LoseLifeTriggeredAbility;
import mage.abilities.dynamicvalue.common.SavedLifeLossValue;
import mage.abilities.effects.common.GainLifeEffect;
import mage.abilities.keyword.DeathtouchAbility;
import mage.abilities.keyword.FlyingAbility;
@ -10,9 +10,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.constants.TargetController;
import java.util.UUID;
@ -36,7 +34,8 @@ public final class BloodthirstyConqueror extends CardImpl {
this.addAbility(DeathtouchAbility.getInstance());
// Whenever an opponent loses life, you gain that much life.
this.addAbility(new BloodthirstyConquerorTriggeredAbility());
this.addAbility(new LoseLifeTriggeredAbility(new GainLifeEffect(SavedLifeLossValue.MUCH),
TargetController.OPPONENT, false, false));
}
private BloodthirstyConqueror(final BloodthirstyConqueror card) {
@ -48,34 +47,3 @@ public final class BloodthirstyConqueror extends CardImpl {
return new BloodthirstyConqueror(this);
}
}
class BloodthirstyConquerorTriggeredAbility extends TriggeredAbilityImpl {
BloodthirstyConquerorTriggeredAbility() {
super(Zone.BATTLEFIELD, new GainLifeEffect(SavedGainedLifeValue.MUCH));
this.setTriggerPhrase("Whenever an opponent loses life, ");
}
private BloodthirstyConquerorTriggeredAbility(final BloodthirstyConquerorTriggeredAbility ability) {
super(ability);
}
@Override
public BloodthirstyConquerorTriggeredAbility copy() {
return new BloodthirstyConquerorTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!game.getOpponents(getControllerId()).contains(event.getTargetId())) {
return false;
}
this.getEffects().setValue(SavedGainedLifeValue.VALUE_KEY, event.getAmount());
return true;
}
}

View file

@ -41,7 +41,7 @@ public final class CuratorBeastie extends CardImpl {
// Colorless creatures you control enter with two additional +1/+1 counters on them.
this.addAbility(new SimpleStaticAbility(new EntersWithCountersControlledEffect(
filter, CounterType.P1P1.createInstance(2), false
)));
).setText("colorless creatures you control enter with two additional +1/+1 counters on them")));
// Whenever Curator Beastie enters or attacks, manifest dread.
this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new ManifestDreadEffect()));

View file

@ -1,16 +1,14 @@
package mage.cards.e;
import java.util.UUID;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.LoseLifeTriggeredAbility;
import mage.abilities.dynamicvalue.common.SavedLifeLossValue;
import mage.abilities.effects.common.GainLifeEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.constants.TargetController;
import java.util.UUID;
/**
* @author noxx
@ -18,12 +16,11 @@ import mage.game.events.GameEvent.EventType;
public final class ExquisiteBlood extends CardImpl {
public ExquisiteBlood(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{4}{B}");
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{B}");
// Whenever an opponent loses life, you gain that much life.
ExquisiteBloodTriggeredAbility ability = new ExquisiteBloodTriggeredAbility();
this.addAbility(ability);
this.addAbility(new LoseLifeTriggeredAbility(new GainLifeEffect(SavedLifeLossValue.MUCH),
TargetController.OPPONENT, false, false));
}
private ExquisiteBlood(final ExquisiteBlood card) {
@ -35,39 +32,3 @@ public final class ExquisiteBlood extends CardImpl {
return new ExquisiteBlood(this);
}
}
class ExquisiteBloodTriggeredAbility extends TriggeredAbilityImpl {
public ExquisiteBloodTriggeredAbility() {
super(Zone.BATTLEFIELD, null);
}
private ExquisiteBloodTriggeredAbility(final ExquisiteBloodTriggeredAbility ability) {
super(ability);
}
@Override
public ExquisiteBloodTriggeredAbility copy() {
return new ExquisiteBloodTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (game.getOpponents(this.controllerId).contains(event.getPlayerId())) {
this.getEffects().clear();
this.addEffect(new GainLifeEffect(event.getAmount()));
return true;
}
return false;
}
@Override
public String getRule() {
return "Whenever an opponent loses life, you gain that much life.";
}
}

View file

@ -46,7 +46,8 @@ public final class GlitchInterpreter extends CardImpl {
this.toughness = new MageInt(3);
// When Glitch Interpreter enters, if you control no face-down permanents, return Glitch Interpreter to its owner's hand and manifest dread.
Ability ability = new EntersBattlefieldTriggeredAbility(new ReturnToHandSourceEffect()).withInterveningIf(condition);
Ability ability = new EntersBattlefieldTriggeredAbility(new ReturnToHandSourceEffect())
.withRuleTextReplacement(false).withInterveningIf(condition);
ability.addEffect(new ManifestDreadEffect().concatBy("and"));
this.addAbility(ability);

View file

@ -1,7 +1,7 @@
package mage.cards.g;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.LoseLifeFirstTimeEachTurnTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.PayEnergyCost;
import mage.abilities.costs.common.SacrificeSourceCost;
@ -10,14 +10,7 @@ import mage.abilities.effects.common.counter.GetEnergyCountersControllerEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.WatcherScope;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.watchers.Watcher;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
@ -29,8 +22,8 @@ public final class GontisMachinations extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{B}");
// Whenever you lose life for the first time each turn, you get {E}.
this.addAbility(new GontisMachinationsTriggeredAbility(),
new GontisMachinationsFirstLostLifeThisTurnWatcher());
this.addAbility(new LoseLifeFirstTimeEachTurnTriggeredAbility(
new GetEnergyCountersControllerEffect(1)));
// Pay {E}{E}, Sacrifice Gonti's Machinations: Each opponent loses 3 life. You gain life equal to the life lost this way.
Ability ability = new SimpleActivatedAbility(
@ -50,68 +43,3 @@ public final class GontisMachinations extends CardImpl {
return new GontisMachinations(this);
}
}
class GontisMachinationsTriggeredAbility extends TriggeredAbilityImpl {
public GontisMachinationsTriggeredAbility() {
super(Zone.BATTLEFIELD, new GetEnergyCountersControllerEffect(1), false);
setTriggerPhrase("Whenever you lose life for the first time each turn, ");
}
private GontisMachinationsTriggeredAbility(final GontisMachinationsTriggeredAbility ability) {
super(ability);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getPlayerId().equals(getControllerId())) {
GontisMachinationsFirstLostLifeThisTurnWatcher watcher
= game.getState().getWatcher(GontisMachinationsFirstLostLifeThisTurnWatcher.class);
if (watcher != null
&& watcher.timesLostLifeThisTurn(event.getPlayerId()) < 2) {
return true;
}
}
return false;
}
@Override
public GontisMachinationsTriggeredAbility copy() {
return new GontisMachinationsTriggeredAbility(this);
}
}
class GontisMachinationsFirstLostLifeThisTurnWatcher extends Watcher {
private final Map<UUID, Integer> playersLostLife = new HashMap<>();
public GontisMachinationsFirstLostLifeThisTurnWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
switch (event.getType()) {
case LOST_LIFE:
int timesLifeLost = playersLostLife.getOrDefault(event.getPlayerId(), 0);
timesLifeLost++;
playersLostLife.put(event.getPlayerId(), timesLifeLost);
}
}
@Override
public void reset() {
super.reset();
playersLostLife.clear();
}
public int timesLostLifeThisTurn(UUID playerId) {
return playersLostLife.getOrDefault(playerId, 0);
}
}

View file

@ -1,12 +1,12 @@
package mage.cards.l;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.GainLifeControllerTriggeredAbility;
import mage.abilities.common.LeavesBattlefieldTriggeredAbility;
import mage.abilities.common.LoseLifeTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.dynamicvalue.common.SavedGainedLifeValue;
import mage.abilities.dynamicvalue.common.SavedLifeLossValue;
import mage.abilities.effects.ContinuousRuleModifyingEffectImpl;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
@ -56,7 +56,7 @@ public final class LichsMastery extends CardImpl {
));
// Whenever you lose life, for each 1 life you lost, exile a permanent you control or a card from your hand or graveyard.
this.addAbility(new LichsMasteryLoseLifeTriggeredAbility());
this.addAbility(new LoseLifeTriggeredAbility(new LichsMasteryLoseLifeEffect()));
// When Lich's Mastery leaves the battlefield, you lose the game.
this.addAbility(new LeavesBattlefieldTriggeredAbility(new LoseGameSourceControllerEffect(), false));
@ -99,57 +99,15 @@ class LichsMasteryCantLoseEffect extends ContinuousRuleModifyingEffectImpl {
}
}
class LichsMasteryLoseLifeTriggeredAbility extends TriggeredAbilityImpl {
public LichsMasteryLoseLifeTriggeredAbility() {
super(Zone.BATTLEFIELD, new LichsMasteryLoseLifeEffect(), false);
}
private LichsMasteryLoseLifeTriggeredAbility(final LichsMasteryLoseLifeTriggeredAbility ability) {
super(ability);
}
@Override
public LichsMasteryLoseLifeTriggeredAbility copy() {
return new LichsMasteryLoseLifeTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getPlayerId().equals(this.getControllerId())) {
for (Effect effect : this.getEffects()) {
if (effect instanceof LichsMasteryLoseLifeEffect) {
((LichsMasteryLoseLifeEffect) effect).setAmount(event.getAmount());
}
}
return true;
}
return false;
}
@Override
public String getRule() {
return "Whenever you lose life, for each 1 life you lost, exile a permanent you control or a card from your hand or graveyard.";
}
}
class LichsMasteryLoseLifeEffect extends OneShotEffect {
private int amount = 0;
public LichsMasteryLoseLifeEffect() {
LichsMasteryLoseLifeEffect() {
super(Outcome.Exile);
this.staticText = "for each 1 life you lost, exile a permanent you control or a card from your hand or graveyard.";
this.staticText = "for each 1 life you lost, exile a permanent you control or a card from your hand or graveyard";
}
private LichsMasteryLoseLifeEffect(final LichsMasteryLoseLifeEffect effect) {
super(effect);
this.amount = effect.amount;
}
@Override
@ -165,7 +123,7 @@ class LichsMasteryLoseLifeEffect extends OneShotEffect {
}
FilterPermanent filter = new FilterPermanent();
filter.add(new ControllerIdPredicate(controller.getId()));
for (int i = 0; i < amount; i++) {
for (int i = 0; i < SavedLifeLossValue.MANY.calculate(game, source, this); i++) {
int handCount = controller.getHand().size();
int graveCount = controller.getGraveyard().size();
int permCount = game.getBattlefield().getActivePermanents(filter, controller.getId(), game).size();
@ -182,7 +140,7 @@ class LichsMasteryLoseLifeEffect extends OneShotEffect {
if (card != null) {
controller.moveCards(card, Zone.EXILED, source, game);
}
} else if (graveCount > 0) {
} else {
Target target = new TargetCardInYourGraveyard(1, 1, new FilterCard(), true);
target.choose(Outcome.Exile, source.getControllerId(), source.getSourceId(), source, game);
Card card = controller.getGraveyard().get(target.getFirstTarget(), game);
@ -194,7 +152,4 @@ class LichsMasteryLoseLifeEffect extends OneShotEffect {
return true;
}
public void setAmount(int amount) {
this.amount = amount;
}
}

View file

@ -1,37 +1,33 @@
package mage.cards.l;
import java.util.UUID;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.LoseLifeTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.dynamicvalue.common.SavedLifeLossValue;
import mage.abilities.effects.common.SacrificeControllerEffect;
import mage.abilities.effects.common.SacrificeEffect;
import mage.abilities.effects.common.continuous.DontLoseByZeroOrLessLifeEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.filter.StaticFilters;
import java.util.UUID;
/**
*
* @author emerald000
*/
public final class LichsTomb extends CardImpl {
public LichsTomb(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT},"{4}");
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}");
// You don't lose the game for having 0 or less life.
this.addAbility(new SimpleStaticAbility(new DontLoseByZeroOrLessLifeEffect(Duration.WhileOnBattlefield)));
// Whenever you lose life, sacrifice a permanent for each 1 life you lost.
this.addAbility(new LichsTombTriggeredAbility());
this.addAbility(new LoseLifeTriggeredAbility(new SacrificeControllerEffect(
StaticFilters.FILTER_PERMANENT, SavedLifeLossValue.MANY, ""
).setText("sacrifice a permanent for each 1 life you lost")));
}
private LichsTomb(final LichsTomb card) {
@ -43,38 +39,3 @@ public final class LichsTomb extends CardImpl {
return new LichsTomb(this);
}
}
class LichsTombTriggeredAbility extends TriggeredAbilityImpl {
LichsTombTriggeredAbility() {
super(Zone.BATTLEFIELD, new SacrificeControllerEffect(new FilterPermanent(), 0, ""), false);
}
private LichsTombTriggeredAbility(final LichsTombTriggeredAbility ability) {
super(ability);
}
@Override
public LichsTombTriggeredAbility copy() {
return new LichsTombTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getPlayerId().equals(this.getControllerId())) {
((SacrificeEffect) this.getEffects().get(0)).setAmount(StaticValue.get(event.getAmount()));
return true;
}
return false;
}
@Override
public String getRule() {
return "Whenever you lose life, sacrifice a permanent for each 1 life you lost.";
}
}

View file

@ -1,16 +1,16 @@
package mage.cards.m;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.GainLifeControllerTriggeredAbility;
import mage.abilities.common.LoseLifeTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.common.CastFromEverywhereSourceCondition;
import mage.abilities.condition.common.HellbentCondition;
import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility;
import mage.abilities.decorator.ConditionalOneShotEffect;
import mage.abilities.dynamicvalue.common.SavedDamageValue;
import mage.abilities.dynamicvalue.common.SavedGainedLifeValue;
import mage.abilities.dynamicvalue.common.SavedLifeLossValue;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.LoseGameSourceControllerEffect;
import mage.abilities.effects.common.continuous.DontLoseByZeroOrLessLifeEffect;
@ -21,9 +21,6 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SuperType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import java.util.UUID;
@ -58,7 +55,12 @@ public final class MarinaVendrellsGrimoire extends CardImpl {
));
// Whenever you lose life, discard that many cards. Then if you have no cards in hand, you lose the game.
this.addAbility(new MarinaVendrellsGrimoireTriggeredAbility());
Ability ability2 = new LoseLifeTriggeredAbility(new DiscardControllerEffect(SavedLifeLossValue.MANY));
ability2.addEffect(new ConditionalOneShotEffect(
new LoseGameSourceControllerEffect(), HellbentCondition.instance,
"Then if you have no cards in hand, you lose the game"
));
this.addAbility(ability2);
}
private MarinaVendrellsGrimoire(final MarinaVendrellsGrimoire card) {
@ -70,38 +72,3 @@ public final class MarinaVendrellsGrimoire extends CardImpl {
return new MarinaVendrellsGrimoire(this);
}
}
class MarinaVendrellsGrimoireTriggeredAbility extends TriggeredAbilityImpl {
MarinaVendrellsGrimoireTriggeredAbility() {
super(Zone.BATTLEFIELD, new DiscardControllerEffect(SavedDamageValue.MANY));
this.addEffect(new ConditionalOneShotEffect(
new LoseGameSourceControllerEffect(), HellbentCondition.instance,
"Then if you have no cards in hand, you lose the game"
));
this.setTriggerPhrase("Whenever you lose life, ");
}
private MarinaVendrellsGrimoireTriggeredAbility(final MarinaVendrellsGrimoireTriggeredAbility ability) {
super(ability);
}
@Override
public MarinaVendrellsGrimoireTriggeredAbility copy() {
return new MarinaVendrellsGrimoireTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!isControlledBy(event.getPlayerId())) {
return false;
}
this.getEffects().setValue("damage", event.getAmount());
return true;
}
}

View file

@ -1,21 +1,13 @@
package mage.cards.m;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.common.LoseLifeTriggeredAbility;
import mage.abilities.dynamicvalue.common.SavedLifeLossValue;
import mage.abilities.effects.common.MillCardsTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.constants.TargetController;
import java.util.Set;
import java.util.UUID;
/**
@ -26,9 +18,11 @@ public final class Mindcrank extends CardImpl {
public Mindcrank(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}");
// Whenever an opponent loses life, that player puts that many cards from the top of their library into their graveyard.
// (Damage dealt by sources without infect causes loss of life.)
this.addAbility(new MindcrankTriggeredAbility());
// Whenever an opponent loses life, that player mills that many cards.
this.addAbility(new LoseLifeTriggeredAbility(
new MillCardsTargetEffect(SavedLifeLossValue.MANY),
TargetController.OPPONENT, false, true
));
}
private Mindcrank(final Mindcrank card) {
@ -40,71 +34,3 @@ public final class Mindcrank extends CardImpl {
return new Mindcrank(this);
}
}
class MindcrankTriggeredAbility extends TriggeredAbilityImpl {
public MindcrankTriggeredAbility() {
super(Zone.BATTLEFIELD, new MindcrankEffect(), false);
}
private MindcrankTriggeredAbility(final MindcrankTriggeredAbility ability) {
super(ability);
}
@Override
public MindcrankTriggeredAbility copy() {
return new MindcrankTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
Set<UUID> opponents = game.getOpponents(this.getControllerId());
if (opponents.contains(event.getPlayerId())) {
for (Effect effect : this.getEffects()) {
effect.setValue("lostLife", event.getAmount());
effect.setTargetPointer(new FixedTarget(event.getPlayerId()));
}
return true;
}
return false;
}
@Override
public String getRule() {
return "Whenever an opponent loses life, that player mills that many cards.";
}
}
class MindcrankEffect extends OneShotEffect {
MindcrankEffect() {
super(Outcome.Detriment);
}
private MindcrankEffect(final MindcrankEffect effect) {
super(effect);
}
@Override
public MindcrankEffect copy() {
return new MindcrankEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player targetPlayer = game.getPlayer(getTargetPointer().getFirst(game, source));
if (targetPlayer != null) {
Integer amount = (Integer) getValue("lostLife");
if (amount == null) {
amount = 0;
}
targetPlayer.millCards(amount, source, game);
}
return true;
}
}

View file

@ -1,31 +1,28 @@
package mage.cards.o;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.LoseLifeTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.common.DiscardTargetCost;
import mage.abilities.costs.common.DiscardCardCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.dynamicvalue.common.SavedLifeLossValue;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.DoUnlessControllerPaysEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.SacrificeControllerEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetCardInHand;
import mage.target.common.TargetControlledPermanent;
import java.util.UUID;
/**
*
* @author jeffwadsworth
*/
public final class OathOfLimDul extends CardImpl {
@ -34,7 +31,7 @@ public final class OathOfLimDul extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{B}");
// Whenever you lose life, for each 1 life you lost, sacrifice a permanent other than Oath of Lim-Dul unless you discard a card.
this.addAbility(new OathOfLimDulTriggeredAbility());
this.addAbility(new LoseLifeTriggeredAbility(new OathOfLimDulEffect()));
// {B}{B}: Draw a card.
this.addAbility(new SimpleActivatedAbility(new DrawCardSourceControllerEffect(1), new ManaCostsImpl<>("{B}{B}")));
@ -51,51 +48,17 @@ public final class OathOfLimDul extends CardImpl {
}
}
class OathOfLimDulTriggeredAbility extends TriggeredAbilityImpl {
public OathOfLimDulTriggeredAbility() {
super(Zone.BATTLEFIELD, new OathOfLimDulEffect());
}
private OathOfLimDulTriggeredAbility(final OathOfLimDulTriggeredAbility ability) {
super(ability);
}
@Override
public OathOfLimDulTriggeredAbility copy() {
return new OathOfLimDulTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getPlayerId().equals(controllerId)) {
game.getState().setValue(sourceId.toString() + "oathOfLimDul", event.getAmount());
return true;
}
return false;
}
@Override
public String getRule() {
return "Whenever you lose life, for each 1 life you lost, sacrifice a permanent other than {this} unless you discard a card.";
}
}
class OathOfLimDulEffect extends OneShotEffect {
private static final FilterControlledPermanent filter = new FilterControlledPermanent("controlled permanent other than Oath of Lim-Dul to sacrifice");
private static final FilterControlledPermanent filter = new FilterControlledPermanent("controlled permanent other than {this} to sacrifice");
static {
filter.add(AnotherPredicate.instance);
}
public OathOfLimDulEffect() {
super(Outcome.Neutral);
OathOfLimDulEffect() {
super(Outcome.Detriment);
staticText = "for each 1 life you lost, sacrifice a permanent other than {this} unless you discard a card";
}
private OathOfLimDulEffect(final OathOfLimDulEffect effect) {
@ -104,34 +67,19 @@ class OathOfLimDulEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
boolean sacrificeDone = false;
int numberSacrificed = 0;
int numberToDiscard = 0;
int numberOfControlledPermanents = 0;
int amountDamage = SavedLifeLossValue.MANY.calculate(game, source, this);
Player controller = game.getPlayer(source.getControllerId());
int amountDamage = (int) game.getState().getValue(source.getSourceId().toString() + "oathOfLimDul");
if (amountDamage > 0
&& controller != null) {
TargetControlledPermanent target = new TargetControlledPermanent(0, numberOfControlledPermanents, filter, true);
target.withNotTarget(true);
if (controller.choose(Outcome.Detriment, target, source, game)) {
for (UUID targetPermanentId : target.getTargets()) {
Permanent permanent = game.getPermanent(targetPermanentId);
if (permanent != null
&& permanent.sacrifice(source, game)) {
numberSacrificed += 1;
sacrificeDone = true;
}
}
}
numberToDiscard = amountDamage - numberSacrificed;
Cost cost = new DiscardTargetCost(new TargetCardInHand(numberToDiscard, new FilterCard("card(s) in your hand to discard")));
if (numberToDiscard > 0
&& cost.canPay(source, source, controller.getId(), game)) {
return cost.pay(source, game, source, controller.getId(), true); // discard cost paid simultaneously
}
if (amountDamage <= 0 || controller == null) {
return false;
}
return sacrificeDone;
boolean didSomething = false;
for (int i = 0; i < amountDamage; ++i) {
didSomething |= new DoUnlessControllerPaysEffect(
new SacrificeControllerEffect(StaticFilters.FILTER_CONTROLLED_ANOTHER_PERMANENT, 1, ""),
new DiscardCardCost()
).apply(game, source);
}
return didSomething;
}
@Override

View file

@ -1,34 +1,34 @@
package mage.cards.t;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.StateTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.LoseLifeTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.MultipliedValue;
import mage.abilities.dynamicvalue.common.SavedLifeLossValue;
import mage.abilities.effects.common.GainLifeEffect;
import mage.abilities.effects.common.LoseGameSourceControllerEffect;
import mage.abilities.effects.common.continuous.DontLoseByZeroOrLessLifeEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.players.Player;
import java.util.UUID;
/**
*
* @author emerald000
*/
public final class Transcendence extends CardImpl {
private static final DynamicValue value = new MultipliedValue(SavedLifeLossValue.MUCH, 2);
public Transcendence(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{3}{W}{W}{W}");
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{W}{W}{W}");
// You don't lose the game for having 0 or less life.
this.addAbility(new SimpleStaticAbility(new DontLoseByZeroOrLessLifeEffect(Duration.WhileOnBattlefield)));
@ -37,7 +37,9 @@ public final class Transcendence extends CardImpl {
this.addAbility(new TranscendenceStateTriggeredAbility());
// Whenever you lose life, you gain 2 life for each 1 life you lost.
this.addAbility(new TranscendenceLoseLifeTriggeredAbility());
this.addAbility(new LoseLifeTriggeredAbility(
new GainLifeEffect(value).setText("you gain 2 life for each 1 life you lost")
));
}
private Transcendence(final Transcendence card) {
@ -79,76 +81,3 @@ class TranscendenceStateTriggeredAbility extends StateTriggeredAbility {
return "When you have 20 or more life, you lose the game.";
}
}
class TranscendenceLoseLifeTriggeredAbility extends TriggeredAbilityImpl {
TranscendenceLoseLifeTriggeredAbility() {
super(Zone.BATTLEFIELD, new TranscendenceLoseLifeEffect(), false);
}
private TranscendenceLoseLifeTriggeredAbility(final TranscendenceLoseLifeTriggeredAbility ability) {
super(ability);
}
@Override
public TranscendenceLoseLifeTriggeredAbility copy() {
return new TranscendenceLoseLifeTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getPlayerId().equals(this.getControllerId())) {
for (Effect effect : this.getEffects()) {
if (effect instanceof TranscendenceLoseLifeEffect) {
((TranscendenceLoseLifeEffect) effect).setAmount(event.getAmount());
}
}
return true;
}
return false;
}
@Override
public String getRule() {
return "Whenever you lose life, you gain 2 life for each 1 life you lost.";
}
}
class TranscendenceLoseLifeEffect extends OneShotEffect {
private int amount = 0;
TranscendenceLoseLifeEffect() {
super(Outcome.GainLife);
this.staticText = "you gain 2 life for each 1 life you lost";
}
private TranscendenceLoseLifeEffect(final TranscendenceLoseLifeEffect effect) {
super(effect);
this.amount = effect.amount;
}
@Override
public TranscendenceLoseLifeEffect copy() {
return new TranscendenceLoseLifeEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
controller.gainLife(2 * amount, game, source);
return true;
}
return false;
}
public void setAmount(int amount) {
this.amount = amount;
}
}

View file

@ -0,0 +1,79 @@
package mage.cards.v;
import mage.MageInt;
import mage.abilities.common.LoseLifeFirstTimeEachTurnTriggeredAbility;
import mage.abilities.costs.common.PayLifeCost;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.WardAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.constants.TargetController;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.GameEvent;
import java.util.UUID;
/**
* @author xenohedron
*/
public final class ValgavothHarrowerOfSouls extends CardImpl {
public ValgavothHarrowerOfSouls(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{R}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.ELDER);
this.subtype.add(SubType.DEMON);
this.power = new MageInt(4);
this.toughness = new MageInt(4);
// Flying
this.addAbility(FlyingAbility.getInstance());
// Ward--Pay 2 life.
this.addAbility(new WardAbility(new PayLifeCost(2)));
// Whenever an opponent loses life for the first time during each of their turns, put a +1/+1 counter on Valgavoth, Harrower of Souls and draw a card.
this.addAbility(new ValgavothHarrowerOfSoulsTriggeredAbility());
}
private ValgavothHarrowerOfSouls(final ValgavothHarrowerOfSouls card) {
super(card);
}
@Override
public ValgavothHarrowerOfSouls copy() {
return new ValgavothHarrowerOfSouls(this);
}
}
class ValgavothHarrowerOfSoulsTriggeredAbility extends LoseLifeFirstTimeEachTurnTriggeredAbility {
ValgavothHarrowerOfSoulsTriggeredAbility() {
super(new AddCountersSourceEffect(CounterType.P1P1.createInstance()), TargetController.OPPONENT, false, false);
this.addEffect(new DrawCardSourceControllerEffect(1).concatBy("and"));
setTriggerPhrase("Whenever an opponent loses life for the first time during each of their turns, ");
}
private ValgavothHarrowerOfSoulsTriggeredAbility(final ValgavothHarrowerOfSoulsTriggeredAbility ability) {
super(ability);
}
@Override
public ValgavothHarrowerOfSoulsTriggeredAbility copy() {
return new ValgavothHarrowerOfSoulsTriggeredAbility(this);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return game.isActivePlayer(event.getTargetId()) && super.checkTrigger(event, game);
}
}

View file

@ -1,8 +1,8 @@
package mage.cards.v;
import mage.MageInt;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.GainLifeControllerTriggeredAbility;
import mage.abilities.common.LoseLifeTriggeredAbility;
import mage.abilities.condition.common.MyTurnCondition;
import mage.abilities.decorator.ConditionalTriggeredAbility;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
@ -11,7 +11,6 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.GameEvent;
@ -54,10 +53,11 @@ public final class VampireScrivener extends CardImpl {
}
}
class VampireScrivenerTriggeredAbility extends TriggeredAbilityImpl {
class VampireScrivenerTriggeredAbility extends LoseLifeTriggeredAbility {
VampireScrivenerTriggeredAbility() {
super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.P1P1.createInstance()));
super(new AddCountersSourceEffect(CounterType.P1P1.createInstance()));
setTriggerPhrase("Whenever you lose life during your turn, ");
}
private VampireScrivenerTriggeredAbility(final VampireScrivenerTriggeredAbility ability) {
@ -69,18 +69,9 @@ class VampireScrivenerTriggeredAbility extends TriggeredAbilityImpl {
return new VampireScrivenerTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return game.isActivePlayer(event.getPlayerId()) && game.isActivePlayer(getControllerId());
return game.isActivePlayer(getControllerId()) && super.checkTrigger(event, game);
}
@Override
public String getRule() {
return "Whenever you lose life during your turn, put a +1/+1 counter on {this}.";
}
}

View file

@ -1,21 +1,14 @@
package mage.cards.v;
import mage.MageInt;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.LoseLifeFirstTimeEachTurnTriggeredAbility;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.WatcherScope;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.watchers.Watcher;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
@ -32,7 +25,8 @@ public final class VengefulWarchief extends CardImpl {
this.toughness = new MageInt(4);
// Whenever you lose life for the first time each turn, put a +1/+1 counter on Vengeful Warchief.
this.addAbility(new VengefulWarchiefTriggeredAbility(), new VengefulWarchiefWatcher());
this.addAbility(new LoseLifeFirstTimeEachTurnTriggeredAbility(
new AddCountersSourceEffect(CounterType.P1P1.createInstance())));
}
private VengefulWarchief(final VengefulWarchief card) {
@ -44,62 +38,3 @@ public final class VengefulWarchief extends CardImpl {
return new VengefulWarchief(this);
}
}
class VengefulWarchiefTriggeredAbility extends TriggeredAbilityImpl {
VengefulWarchiefTriggeredAbility() {
super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.P1P1.createInstance()), false);
setTriggerPhrase("Whenever you lose life for the first time each turn, ");
}
private VengefulWarchiefTriggeredAbility(final VengefulWarchiefTriggeredAbility ability) {
super(ability);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!event.getPlayerId().equals(getControllerId())) {
return false;
}
VengefulWarchiefWatcher watcher = game.getState().getWatcher(VengefulWarchiefWatcher.class);
return watcher != null && watcher.timesLostLifeThisTurn(event.getPlayerId()) < 2;
}
@Override
public VengefulWarchiefTriggeredAbility copy() {
return new VengefulWarchiefTriggeredAbility(this);
}
}
class VengefulWarchiefWatcher extends Watcher {
private final Map<UUID, Integer> playersLostLife = new HashMap<>();
VengefulWarchiefWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.LOST_LIFE) {
int timesLifeLost = playersLostLife.getOrDefault(event.getPlayerId(), 0);
timesLifeLost++;
playersLostLife.put(event.getPlayerId(), timesLifeLost);
}
}
@Override
public void reset() {
super.reset();
playersLostLife.clear();
}
int timesLostLifeThisTurn(UUID playerId) {
return playersLostLife.getOrDefault(playerId, 0);
}
}

View file

@ -2,10 +2,11 @@ package mage.cards.v;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.LoseLifeTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.PayLifeCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.dynamicvalue.common.SavedLifeLossValue;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.continuous.BoostTargetEffect;
import mage.abilities.keyword.FlyingAbility;
@ -14,9 +15,6 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
@ -46,7 +44,7 @@ public final class VilisBrokerOfBlood extends CardImpl {
this.addAbility(ability);
// Whenever you lose life, draw that many cards.
this.addAbility(new VilisBrokerOfBloodTriggeredAbility());
this.addAbility(new LoseLifeTriggeredAbility(new DrawCardSourceControllerEffect(SavedLifeLossValue.MANY)));
}
private VilisBrokerOfBlood(final VilisBrokerOfBlood card) {
@ -58,39 +56,3 @@ public final class VilisBrokerOfBlood extends CardImpl {
return new VilisBrokerOfBlood(this);
}
}
class VilisBrokerOfBloodTriggeredAbility extends TriggeredAbilityImpl {
VilisBrokerOfBloodTriggeredAbility() {
super(Zone.BATTLEFIELD, null, false);
}
private VilisBrokerOfBloodTriggeredAbility(final VilisBrokerOfBloodTriggeredAbility ability) {
super(ability);
}
@Override
public VilisBrokerOfBloodTriggeredAbility copy() {
return new VilisBrokerOfBloodTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getPlayerId().equals(this.getControllerId())) {
this.getEffects().clear();
this.addEffect(new DrawCardSourceControllerEffect(event.getAmount()));
return true;
}
return false;
}
@Override
public String getRule() {
return "Whenever you lose life, draw that many cards.";
}
}

View file

@ -283,6 +283,7 @@ public final class DuskmournHouseOfHorrorCommander extends ExpansionSet {
cards.add(new SetCardInfo("Twilight Mire", 320, Rarity.RARE, mage.cards.t.TwilightMire.class));
cards.add(new SetCardInfo("Underground River", 321, Rarity.RARE, mage.cards.u.UndergroundRiver.class));
cards.add(new SetCardInfo("Utter End", 91, Rarity.RARE, mage.cards.u.UtterEnd.class));
cards.add(new SetCardInfo("Valgavoth, Harrower of Souls", 6, Rarity.MYTHIC, mage.cards.v.ValgavothHarrowerOfSouls.class));
cards.add(new SetCardInfo("Vault of Whispers", 322, Rarity.COMMON, mage.cards.v.VaultOfWhispers.class));
cards.add(new SetCardInfo("Verge Rangers", 108, Rarity.RARE, mage.cards.v.VergeRangers.class));
cards.add(new SetCardInfo("Vial Smasher the Fierce", 239, Rarity.MYTHIC, mage.cards.v.VialSmasherTheFierce.class));

View file

@ -1,4 +1,4 @@
package org.mage.test.cards.watchers;
package org.mage.test.cards.single.aer;
import mage.constants.PhaseStep;
import mage.constants.Zone;
@ -7,7 +7,6 @@ import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author escplan9
*/
public class GontisMachinationsTest extends CardTestPlayerBase {
@ -19,57 +18,59 @@ Whenever you lose life for the first time each turn, you get {E}. (You get an en
Pay {E}{E}, Sacrifice Gonti's Machinations: Each opponent loses 3 life. You gain life equal to the life lost this way.
*/
private final String gMachinations = "Gonti's Machinations";
/*
* Reported bug: [[Gonti's Machinations]] currently triggers and gain 1 energy whenever you lose life instead of only the first life loss of each turn.
* See issue #3499 (test is currently failing due to bug in code)
*/
* See issue #3499 for context
*/
@Test
public void machinations_ThreeCreaturesCombatDamage_OneTrigger() {
setStrictChooseMode(true);
String memnite = "Memnite"; // {0} 1/1
String gBears = "Grizzly Bears"; // {1}{G} 2/2
String hGiant = "Hill Giant"; // {2}{R} 3/3
addCard(Zone.BATTLEFIELD, playerB, gMachinations);
addCard(Zone.BATTLEFIELD, playerA, memnite);
addCard(Zone.BATTLEFIELD, playerA, gBears);
addCard(Zone.BATTLEFIELD, playerA, hGiant);
attack(3, playerA, memnite);
attack(3, playerA, gBears);
attack(3, playerA, hGiant);
setStopAt(3, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertTapped(memnite, true);
assertTapped(gBears, true);
assertTapped(hGiant, true);
assertLife(playerB, 14); // 1 + 2 + 3 damage
assertCounterCount(playerB, CounterType.ENERGY, 1);
}
/*
* Reported bug: [[Gonti's Machinations]] currently triggers and gain 1 energy whenever you lose life instead of only the first life loss of each turn.
* See issue #3499 (test is currently failing due to bug in code)
*/
* See issue #3499 for context
*/
@Test
public void machinations_NonCombatDamageThreeTimes_OneTrigger() {
setStrictChooseMode(true);
String bolt = "Lightning Bolt"; // {R} deal 3
addCard(Zone.BATTLEFIELD, playerB, gMachinations);
addCard(Zone.HAND, playerA, bolt, 3);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertTappedCount("Mountain", true, 3);
assertGraveyardCount(playerA, bolt, 3);
assertLife(playerB, 11); // 3 x 3 damage

View file

@ -7,13 +7,14 @@ import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author noxx
*/
public class ExquisiteBloodTest extends CardTestPlayerBase {
@Test
public void BasicCardTest() {
public void basicCardTest() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
@ -45,10 +46,12 @@ public class ExquisiteBloodTest extends CardTestPlayerBase {
}
/**
* Ajani, Inspiring leader does not trigger Exquisite Blood + Defiant Bloodlord #6464
* Ajani, Inspiring leader does not trigger Exquisite Blood + Defiant Bloodlord #6464
*/
@Test
public void triggerCascadeTest() {
setStrictChooseMode(true);
// +2: You gain 2 life. Put two +1/+1 counters on up to one target creature.
// 3: Exile target creature. Its controller gains 2 life.
// 10: Creatures you control gain flying and double strike until end of turn.
@ -57,14 +60,12 @@ public class ExquisiteBloodTest extends CardTestPlayerBase {
// Flying
// Whenever you gain life, target opponent loses that much life.
addCard(Zone.BATTLEFIELD, playerA, "Defiant Bloodlord", 1); // Creature 4/5 {5}{B}{B}
// Whenever an opponent loses life, you gain that much life.
addCard(Zone.BATTLEFIELD, playerA, "Exquisite Blood", 1); // Enchantment {4}{B}
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+2:", "Defiant Bloodlord");
addTarget(playerA, playerB); // Target opponent of Defiant Bloodlord triggered ability (looping until opponent is dead)
addTarget(playerA, playerB);
addTarget(playerA, playerB);
addTarget(playerA, playerB);
addTarget(playerA, playerB);
addTarget(playerA, playerB);
@ -72,7 +73,9 @@ public class ExquisiteBloodTest extends CardTestPlayerBase {
addTarget(playerA, playerB);
addTarget(playerA, playerB);
addTarget(playerA, playerB);
addTarget(playerA, playerB);
addTarget(playerA, playerB);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
@ -82,15 +85,17 @@ public class ExquisiteBloodTest extends CardTestPlayerBase {
assertLife(playerB, 0); // Player B is dead, game ends
assertLife(playerA, 40);
}
/**
* Ajani, Inspiring leader does not trigger Exquisite Blood + Defiant Bloodlord #6464
* Ajani, Inspiring leader does not trigger Exquisite Blood + Defiant Bloodlord #6464
*/
@Test
public void triggerCascadeAjaniSecondAbilityTest() {
setStrictChooseMode(true);
// +2: You gain 2 life. Put two +1/+1 counters on up to one target creature.
// 3: Exile target creature. Its controller gains 2 life.
// 10: Creatures you control gain flying and double strike until end of turn.
@ -100,14 +105,12 @@ public class ExquisiteBloodTest extends CardTestPlayerBase {
// Flying
// Whenever you gain life, target opponent loses that much life.
addCard(Zone.BATTLEFIELD, playerA, "Defiant Bloodlord", 1); // Creature 4/5 {5}{B}{B}
// Whenever an opponent loses life, you gain that much life.
addCard(Zone.BATTLEFIELD, playerA, "Exquisite Blood", 1); // Enchantment {4}{B}
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-3:", "Silvercoat Lion");
addTarget(playerA, playerB); // Target opponent of Defiant Bloodlord triggered ability (looping until opponent is dead)
addTarget(playerA, playerB);
addTarget(playerA, playerB);
addTarget(playerA, playerB);
addTarget(playerA, playerB);
addTarget(playerA, playerB);
@ -115,7 +118,9 @@ public class ExquisiteBloodTest extends CardTestPlayerBase {
addTarget(playerA, playerB);
addTarget(playerA, playerB);
addTarget(playerA, playerB);
addTarget(playerA, playerB);
addTarget(playerA, playerB);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
@ -126,7 +131,28 @@ public class ExquisiteBloodTest extends CardTestPlayerBase {
assertLife(playerB, 0); // Player B is dead, game ends
assertLife(playerA, 40);
}
@Test
public void attackWithTwoCreatures() {
setStrictChooseMode(true);
// Whenever an opponent loses life, you gain that much life.
addCard(Zone.BATTLEFIELD, playerA, "Exquisite Blood", 1);
addCard(Zone.BATTLEFIELD, playerA, "Elite Vanguard");
addCard(Zone.BATTLEFIELD, playerA, "Memnite");
attack(1, playerA, "Elite Vanguard", playerB);
attack(1, playerA, "Memnite", playerB);
// no trigger stacking, only 1 trigger
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 2 - 1);
assertLife(playerA, 20 + 3);
}
}

View file

@ -0,0 +1,116 @@
package org.mage.test.cards.single.ice;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Susucr
*/
public class OathOfLimDulTest extends CardTestPlayerBase {
/**
* {@link mage.cards.o.OathOfLimDul Oath of Lim-Dûl} {3}{B}
* Enchantment
* Whenever you lose life, for each 1 life you lost, sacrifice a permanent other than Oath of Lim-Dûl unless you discard a card. (Damage dealt to you causes you to lose life.)
* {B}{B}: Draw a card.
*/
private static final String oath = "Oath of Lim-Dul";
@Test
public void test_3Sacrifice() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, oath, 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
addCard(Zone.HAND, playerA, "Swamp", 5);
addCard(Zone.HAND, playerA, "Lightning Bolt");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA);
setChoice(playerA, false); // No to discard on first instance.
setChoice(playerA, "Mountain"); // sacrifice Mountain
setChoice(playerA, false); // No to discard on second instance.
setChoice(playerA, "Mountain"); // sacrifice Mountain
setChoice(playerA, false); // No to discard on third instance.
setChoice(playerA, "Mountain"); // sacrifice Mountain
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerA, 20 - 3);
assertGraveyardCount(playerA, "Mountain", 3);
}
@Test
public void test_3Discard() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, oath, 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
addCard(Zone.HAND, playerA, "Swamp", 5);
addCard(Zone.HAND, playerA, "Lightning Bolt");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA);
setChoice(playerA, true); // Yes to discard on first instance.
setChoice(playerA, "Swamp"); // sacrifice Swamp
setChoice(playerA, true); // Yes to discard on second instance.
setChoice(playerA, "Swamp"); // sacrifice Swamp
setChoice(playerA, true); // Yes to discard on third instance.
setChoice(playerA, "Swamp"); // sacrifice Swamp
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerA, 20 - 3);
assertGraveyardCount(playerA, "Swamp", 3);
}
@Test
public void test_1Sacrifice1Discard_NoOther() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, oath, 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
addCard(Zone.HAND, playerA, "Swamp", 1);
addCard(Zone.HAND, playerA, "Lightning Bolt");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA);
setChoice(playerA, true); // Yes to discard on first instance.
setChoice(playerA, "Swamp"); // discard Swamp
// No more possibility to Discard
setChoice(playerA, "Mountain"); // sacrifice Mountain
// No more things to Sacrifice
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerA, 20 - 3);
assertGraveyardCount(playerA, "Mountain", 1);
assertGraveyardCount(playerA, "Swamp", 1);
}
@Test
public void test_AllSacrificeNoDiscard_KeepCardInHand() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, oath, 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
addCard(Zone.HAND, playerA, "Swamp", 1);
addCard(Zone.HAND, playerA, "Lightning Bolt");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA);
setChoice(playerA, false); // No to discard on first instance.
setChoice(playerA, "Mountain"); // sacrifice Mountain
setChoice(playerA, false); // No to discard on second instance.
setChoice(playerA, false); // No to discard on third instance.
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerA, 20 - 3);
assertPermanentCount(playerA, oath, 1);
assertGraveyardCount(playerA, "Mountain", 1);
assertHandCount(playerA, "Swamp", 1);
}
}

View file

@ -0,0 +1,79 @@
package org.mage.test.cards.single.snc;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Susucr
*/
public class VampireScrivenerTest extends CardTestPlayerBase {
/**
* {@link mage.cards.v.VampireScrivener Vampire Scrivener} {4}{B}
* Creature Vampire Warlock
* Flying
* Whenever you gain life during your turn, put a +1/+1 counter on Vampire Scrivener.
* Whenever you lose life during your turn, put a +1/+1 counter on Vampire Scrivener.
* 2/2
*/
private static final String scrivener = "Vampire Scrivener";
@Test
public void test_LoseLife_Twice() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, scrivener, 1);
addCard(Zone.BATTLEFIELD, playerA, "Battlefield Forge"); // painland
addCard(Zone.HAND, playerA, "Lightning Bolt");
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); // cause 1 trigger
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA); // cause 1 trigger
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerA, 20 - 3 - 1);
assertCounterCount(playerA, scrivener, CounterType.P1P1, 2);
}
@Test
public void test_RakdosCharm() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, scrivener, 1);
addCard(Zone.BATTLEFIELD, playerA, "Kobolds of Kher Keep", 3);
addCard(Zone.BATTLEFIELD, playerA, "Badlands", 2);
addCard(Zone.HAND, playerA, "Rakdos Charm");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rakdos Charm");
setModeChoice(playerA, "3"); // Choose third mode
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerA, 20 - 4);
assertCounterCount(playerA, scrivener, CounterType.P1P1, 1);
}
@Test
public void test_RakdosCharm_NotYourTurn() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, scrivener, 1);
addCard(Zone.BATTLEFIELD, playerA, "Kobolds of Kher Keep", 3);
addCard(Zone.BATTLEFIELD, playerA, "Badlands", 2);
addCard(Zone.HAND, playerA, "Rakdos Charm");
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Rakdos Charm");
setModeChoice(playerA, "3"); // Choose third mode
setStopAt(2, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerA, 20 - 4);
assertCounterCount(playerA, scrivener, CounterType.P1P1, 0); // No trigger, as not your turn.
}
}

View file

@ -28,12 +28,12 @@ public class GainLoseLifeYourTurnTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.GAINED_LIFE
|| event.getType() == GameEvent.EventType.LOST_LIFE;
|| event.getType() == GameEvent.EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return isControlledBy(game.getActivePlayerId())
&& isControlledBy(event.getPlayerId());
&& isControlledBy(event.getTargetId());
}
}

View file

@ -0,0 +1,51 @@
package mage.abilities.common;
import mage.abilities.effects.Effect;
import mage.constants.TargetController;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.watchers.common.LifeLostThisTurnWatcher;
/**
* @author Susucr
*/
public class LoseLifeFirstTimeEachTurnTriggeredAbility extends LoseLifeTriggeredAbility {
public LoseLifeFirstTimeEachTurnTriggeredAbility(Effect effect) {
this(effect, TargetController.YOU, false, false);
}
public LoseLifeFirstTimeEachTurnTriggeredAbility(Effect effect, TargetController targetController, boolean optional, boolean setTargetPointer) {
super(effect, targetController, optional, setTargetPointer);
addWatcher(new LifeLostThisTurnWatcher());
}
protected LoseLifeFirstTimeEachTurnTriggeredAbility(final LoseLifeFirstTimeEachTurnTriggeredAbility ability) {
super(ability);
}
@Override
public LoseLifeFirstTimeEachTurnTriggeredAbility copy() {
return new LoseLifeFirstTimeEachTurnTriggeredAbility(this);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
LifeLostThisTurnWatcher watcher = game.getState().getWatcher(LifeLostThisTurnWatcher.class);
return watcher != null
&& watcher.timesLostLifeThisTurn(event.getTargetId()) <= 1
&& super.checkTrigger(event, game);
}
@Override
protected String generateTriggerPhrase() {
switch (targetController) {
case YOU:
return "Whenever you lose life for the first time each turn, ";
case OPPONENT:
return "Whenever an opponent loses life for the first time each turn, ";
default:
throw new IllegalArgumentException("Wrong code usage: not supported targetController: " + targetController);
}
}
}

View file

@ -0,0 +1,85 @@
package mage.abilities.common;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.dynamicvalue.common.SavedLifeLossValue;
import mage.abilities.effects.Effect;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.LifeLostEvent;
import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
/**
* @author Susucr
*/
public class LoseLifeTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<LifeLostEvent> {
protected final TargetController targetController;
private final boolean setTargetPointer;
public LoseLifeTriggeredAbility(Effect effect) {
this(effect, TargetController.YOU, false, false);
}
public LoseLifeTriggeredAbility(Effect effect, TargetController targetController, boolean optional, boolean setTargetPointer) {
super(Zone.BATTLEFIELD, effect, optional);
this.targetController = targetController;
this.setTargetPointer = setTargetPointer;
setTriggerPhrase(generateTriggerPhrase());
}
protected LoseLifeTriggeredAbility(final LoseLifeTriggeredAbility ability) {
super(ability);
this.targetController = ability.targetController;
this.setTargetPointer = ability.setTargetPointer;
}
@Override
public LoseLifeTriggeredAbility copy() {
return new LoseLifeTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER;
}
private boolean filterPlayer(UUID playerId, Game game) {
switch (targetController) {
case YOU:
return isControlledBy(playerId);
case OPPONENT:
return game.getOpponents(getControllerId()).contains(playerId);
default:
throw new IllegalArgumentException("Wrong code usage: not supported targetController: " + targetController);
}
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!filterPlayer(event.getTargetId(), game)) {
return false;
}
// if target id matches, all events in the batch are relevant
this.getEffects().setValue(SavedLifeLossValue.getValueKey(), event.getAmount());
if (setTargetPointer) {
this.getEffects().setTargetPointer(new FixedTarget(event.getTargetId()));
}
return true;
}
protected String generateTriggerPhrase() {
switch (targetController) {
case YOU:
return "Whenever you lose life, ";
case OPPONENT:
return "Whenever an opponent loses life, ";
default:
throw new IllegalArgumentException("Wrong code usage: not supported targetController: " + targetController);
}
}
}

View file

@ -0,0 +1,49 @@
package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.game.Game;
/**
* @author Susucr
*/
public enum SavedLifeLossValue implements DynamicValue {
MANY("many"),
MUCH("much");
private final String message;
private static final String key = "SavedLifeLoss";
/**
* value key used to store the amount of life lost
*/
public static String getValueKey() {
return key;
}
SavedLifeLossValue(String message) {
this.message = "that " + message;
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return (Integer) effect.getValue(getValueKey());
}
@Override
public SavedLifeLossValue copy() {
return this;
}
@Override
public String toString() {
return message;
}
@Override
public String getMessage() {
return "";
}
}

View file

@ -975,12 +975,17 @@ public class GameState implements Serializable, Copyable<GameState> {
// Combine multiple life loss events in the single event (batch)
// see GameEvent.LOST_LIFE_BATCH
// existing batch
// existing batchs
boolean isLifeLostBatchUsed = false;
boolean isSingleBatchUsed = false;
for (GameEvent event : simultaneousEvents) {
if (event instanceof LifeLostBatchEvent) {
((LifeLostBatchEvent) event).addEvent(lifeLossEvent);
isLifeLostBatchUsed = true;
} else if (event instanceof LifeLostBatchForOnePlayerEvent
&& event.getTargetId().equals(lifeLossEvent.getTargetId())) {
((LifeLostBatchForOnePlayerEvent) event).addEvent(lifeLossEvent);
isSingleBatchUsed = true;
}
}
@ -988,6 +993,9 @@ public class GameState implements Serializable, Copyable<GameState> {
if (!isLifeLostBatchUsed) {
addSimultaneousEvent(new LifeLostBatchEvent(lifeLossEvent), game);
}
if (!isSingleBatchUsed) {
addSimultaneousEvent(new LifeLostBatchForOnePlayerEvent(lifeLossEvent), game);
}
}
public void addSimultaneousTappedToBatch(TappedEvent tappedEvent, Game game) {

View file

@ -166,7 +166,6 @@ public class GameEvent implements Serializable {
DAMAGE_CAUSES_LIFE_LOSS,
PLAYER_LIFE_CHANGE,
GAIN_LIFE, GAINED_LIFE,
LOSE_LIFE, LOST_LIFE,
/* LOSE_LIFE + LOST_LIFE
targetId the id of the player loosing life
sourceId sourceId of the ability which caused the lose
@ -174,10 +173,17 @@ public class GameEvent implements Serializable {
amount amount of life loss
flag true = from combat damage - other from non combat damage
*/
LOST_LIFE_BATCH(true),
LOSE_LIFE, LOST_LIFE,
/* LOST_LIFE_BATCH_FOR_ONE_PLAYER
combines all life lost events for a player to a single batch (event)
*/
LOST_LIFE_BATCH_FOR_ONE_PLAYER(true),
/* LOST_LIFE_BATCH
combines all player life lost events to a single batch (event)
*/
LOST_LIFE_BATCH(true),
PLAY_LAND, LAND_PLAYED,
CREATURE_CHAMPIONED,
/* CREATURE_CHAMPIONED

View file

@ -0,0 +1,11 @@
package mage.game.events;
/**
* @author Susucr
*/
public class LifeLostBatchForOnePlayerEvent extends BatchEvent<LifeLostEvent> {
public LifeLostBatchForOnePlayerEvent(LifeLostEvent firstEvent) {
super(EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER, true, false, false, firstEvent);
}
}

View file

@ -0,0 +1,42 @@
package mage.watchers.common;
import mage.constants.WatcherScope;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.util.CardUtil;
import mage.watchers.Watcher;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author Susucr
*/
public class LifeLostThisTurnWatcher extends Watcher {
// player -> number of times (not amount!) that player lost life this turn.
private final Map<UUID, Integer> playersLostLife = new HashMap<>();
public LifeLostThisTurnWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER) {
playersLostLife.compute(event.getTargetId(), CardUtil::setOrIncrementValue);
}
}
@Override
public void reset() {
super.reset();
playersLostLife.clear();
}
public int timesLostLifeThisTurn(UUID playerId) {
return playersLostLife.getOrDefault(playerId, 0);
}
}