diff --git a/Mage.Sets/src/mage/cards/o/ObNixilisCaptiveKingpin.java b/Mage.Sets/src/mage/cards/o/ObNixilisCaptiveKingpin.java new file mode 100644 index 00000000000..2d0915b14de --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/ObNixilisCaptiveKingpin.java @@ -0,0 +1,105 @@ +package mage.cards.o; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.ExileTopXMayPlayUntilEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.constants.*; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.*; +import mage.util.CardUtil; + +/** + * + * @author jimga150 + */ +public final class ObNixilisCaptiveKingpin extends CardImpl { + + public ObNixilisCaptiveKingpin(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.DEMON); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Whenever one or more opponents each lose exactly 1 life, put a +1/+1 counter on Ob Nixilis, Captive Kingpin. Exile the top card of your library. Until your next end step, you may play that card. + Ability ability = new ObNixilisCaptiveKingpinAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()) + ); + ability.addEffect(new ExileTopXMayPlayUntilEffect(1, Duration.UntilYourNextEndStep) + .withTextOptions("that card", false)); + + this.addAbility(ability); + + } + + private ObNixilisCaptiveKingpin(final ObNixilisCaptiveKingpin card) { + super(card); + } + + @Override + public ObNixilisCaptiveKingpin copy() { + return new ObNixilisCaptiveKingpin(this); + } +} + +class ObNixilisCaptiveKingpinAbility extends TriggeredAbilityImpl { + + ObNixilisCaptiveKingpinAbility(Effect effect) { + super(Zone.BATTLEFIELD, effect); + setTriggerPhrase("Whenever one or more opponents each lose exactly 1 life, "); + } + + private ObNixilisCaptiveKingpinAbility(final ObNixilisCaptiveKingpinAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.LOST_LIFE_BATCH; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + + LifeLostBatchEvent lifeLostBatchEvent = (LifeLostBatchEvent) event; + + boolean opponentLostLife = false; + boolean allis1 = true; + + for (UUID targetPlayer : CardUtil.getEventTargets(lifeLostBatchEvent)){ + // skip controller + if (targetPlayer.equals(getControllerId())){ + continue; + } + opponentLostLife = true; + + int lifelost = lifeLostBatchEvent.getLifeLostByPlayer(targetPlayer); + if (lifelost != 1){ + allis1 = false; + break; + } + } + return opponentLostLife && allis1; + } + + @Override + public ObNixilisCaptiveKingpinAbility copy() { + return new ObNixilisCaptiveKingpinAbility(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MarchOfTheMachineTheAftermath.java b/Mage.Sets/src/mage/sets/MarchOfTheMachineTheAftermath.java index b26f67945c1..3a5121910c7 100644 --- a/Mage.Sets/src/mage/sets/MarchOfTheMachineTheAftermath.java +++ b/Mage.Sets/src/mage/sets/MarchOfTheMachineTheAftermath.java @@ -150,6 +150,7 @@ public final class MarchOfTheMachineTheAftermath extends ExpansionSet { cards.add(new SetCardInfo("Niv-Mizzet, Supreme", 219, Rarity.RARE, mage.cards.n.NivMizzetSupreme.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Niv-Mizzet, Supreme", 40, Rarity.RARE, mage.cards.n.NivMizzetSupreme.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Niv-Mizzet, Supreme", 90, Rarity.RARE, mage.cards.n.NivMizzetSupreme.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ob Nixilis, Captive Kingpin", 41, Rarity.MYTHIC, mage.cards.o.ObNixilisCaptiveKingpin.class)); cards.add(new SetCardInfo("Open the Way", 123, Rarity.RARE, mage.cards.o.OpenTheWay.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Open the Way", 163, Rarity.RARE, mage.cards.o.OpenTheWay.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Open the Way", 23, Rarity.RARE, mage.cards.o.OpenTheWay.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/ObNixilisCaptiveKingpinTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/ObNixilisCaptiveKingpinTest.java new file mode 100644 index 00000000000..7dfd9b83cac --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/ObNixilisCaptiveKingpinTest.java @@ -0,0 +1,204 @@ +package org.mage.test.cards.triggers.damage; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4Players; + +public class ObNixilisCaptiveKingpinTest extends CardTestCommander4Players { + + // - 1 opponent dealt 1 damage -> Ob Nixilis triggers + // - 1 opponent dealt 2 damage -> No trigger + // - 2 opponents dealt 1 damage each -> Ob Nixilis triggers + // - 2 opponents dealt 2 damage each -> No trigger + // - opponent pays 1 life-> Ob Nixilis triggers + // - opponent pays 2 life -> No trigger + // - 1 opponent loses 1 life -> Ob Nixilis triggers + // - 1 opponent loses 2 life -> No trigger + // - 2 opponents lose 1 life each -> Ob Nixilis triggers + // - 2 opponents lose 2 life each -> No trigger + // - controller loses 1 life -> No trigger + + @Test + public void damageController1Point() { + addCard(Zone.BATTLEFIELD, playerA, "Ob Nixilis, Captive Kingpin", 1); + addCard(Zone.BATTLEFIELD, playerD, "Memnite"); + + attack(2, playerD, "Memnite", playerA); + + setStopAt(2, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 0); + } + + @Test + public void damage1Opp1Point() { + addCard(Zone.BATTLEFIELD, playerA, "Ob Nixilis, Captive Kingpin", 1); + addCard(Zone.BATTLEFIELD, playerA, "Memnite"); + + attack(1, playerA, "Memnite", playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 1); + } + + @Test + public void damage1Opp2Points() { + addCard(Zone.BATTLEFIELD, playerA, "Ob Nixilis, Captive Kingpin", 1); + addCard(Zone.BATTLEFIELD, playerA, "Expedition Envoy"); + + attack(1, playerA, "Expedition Envoy", playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 0); + } + + @Test + public void damage2Opp1Point() { + addCard(Zone.BATTLEFIELD, playerA, "Ob Nixilis, Captive Kingpin", 1); + addCard(Zone.BATTLEFIELD, playerA, "Memnite", 2); + + attack(1, playerA, "Memnite", playerB); + attack(1, playerA, "Memnite", playerC); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 1); + } + + @Test + public void damage2Opp2Points() { + addCard(Zone.BATTLEFIELD, playerA, "Ob Nixilis, Captive Kingpin", 1); + addCard(Zone.BATTLEFIELD, playerA, "Expedition Envoy", 2); + + attack(1, playerA, "Expedition Envoy", playerB); + attack(1, playerA, "Expedition Envoy", playerC); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 0); + } + + @Test + public void payLife1Opp1Point() { + addCard(Zone.BATTLEFIELD, playerA, "Ob Nixilis, Captive Kingpin", 1); + addCard(Zone.BATTLEFIELD, playerB, "Arid Mesa"); +// addCard(Zone.LIBRARY, playerA, "Mountain"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{T}, Pay 1 life"); + + addTarget(playerB, "Mountain"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 1); + } + + @Test + public void payLife1Opp2Point() { + addCard(Zone.BATTLEFIELD, playerA, "Ob Nixilis, Captive Kingpin", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Forest", 2); + + // {2}, Pay 2 life: Draw a card. + addCard(Zone.BATTLEFIELD, playerB, "Book of Rass"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{2}, Pay 2 life"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 0); + } + + @Test + public void loseLife1Opp1Point() { + addCard(Zone.BATTLEFIELD, playerA, "Ob Nixilis, Captive Kingpin", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + + // {1}{B}, {T}: Target player loses 1 life. + addCard(Zone.BATTLEFIELD, playerA, "Acolyte of Xathrid"); + + addTarget(playerA, playerC); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{B}, {T}"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 1); + } + + @Test + public void loseLife1Opp2Point() { + addCard(Zone.BATTLEFIELD, playerA, "Ob Nixilis, Captive Kingpin", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + + // Target player draws two cards and loses 2 life. + addCard(Zone.HAND, playerA, "Blood Pact"); + + addTarget(playerA, playerD); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Blood Pact"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 0); + } + + @Test + public void loseLifeAll1Point() { + addCard(Zone.BATTLEFIELD, playerA, "Ob Nixilis, Captive Kingpin", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + + // {2}{W}: Target player gains 1 life. + // {2}{B}: Each player loses 1 life. + addCard(Zone.BATTLEFIELD, playerA, "Orzhov Guildmage"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{B}"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 1); + } + + @Test + public void loseLifeAll2Point() { + addCard(Zone.BATTLEFIELD, playerA, "Ob Nixilis, Captive Kingpin", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + + // Each player loses 2 life. You draw two cards. + addCard(Zone.HAND, playerA, "Crushing Disappointment"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Crushing Disappointment"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 0); + } + +} + diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 21158cc1c07..d6200769503 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -808,6 +808,25 @@ public class GameState implements Serializable, Copyable { return !simultaneousEvents.isEmpty(); } + public void addSimultaneousLifeLossEventToBatches(LifeLostEvent lifeLossEvent, Game game) { + // Combine multiple life loss events in the single event (batch) + // see GameEvent.LOST_LIFE_BATCH + + // existing batch + boolean isLifeLostBatchUsed = false; + for (GameEvent event : simultaneousEvents) { + if (event instanceof LifeLostBatchEvent) { + ((LifeLostBatchEvent) event).addEvent(lifeLossEvent); + isLifeLostBatchUsed = true; + } + } + + // new batch + if (!isLifeLostBatchUsed) { + addSimultaneousEvent(new LifeLostBatchEvent(lifeLossEvent), game); + } + } + public void addSimultaneousDamage(DamagedEvent damagedEvent, Game game) { // Combine multiple damage events in the single event (batch) // * per damage type (see GameEvent.DAMAGED_BATCH_FOR_PERMANENTS, GameEvent.DAMAGED_BATCH_FOR_PLAYERS) diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 4a41084d94a..2ec2a860c5d 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -142,6 +142,10 @@ public class GameEvent implements Serializable { amount amount of life loss flag true = from combat damage - other from non combat damage */ + LOST_LIFE_BATCH, + /* LOST_LIFE_BATCH + combines all player life lost events to a single batch (event) + */ PLAY_LAND, LAND_PLAYED, CREATURE_CHAMPIONED, /* CREATURE_CHAMPIONED diff --git a/Mage/src/main/java/mage/game/events/LifeLostBatchEvent.java b/Mage/src/main/java/mage/game/events/LifeLostBatchEvent.java new file mode 100644 index 00000000000..c5969bd6b4e --- /dev/null +++ b/Mage/src/main/java/mage/game/events/LifeLostBatchEvent.java @@ -0,0 +1,69 @@ +package mage.game.events; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author jimga150 + */ +public class LifeLostBatchEvent extends GameEvent implements BatchGameEvent { + + private final Set events = new HashSet<>(); + + public LifeLostBatchEvent(LifeLostEvent event) { + super(EventType.LOST_LIFE_BATCH, null, null, null); + addEvent(event); + } + + @Override + public Set getEvents() { + return events; + } + + @Override + public Set getTargets() { + return events.stream() + .map(GameEvent::getTargetId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + @Override + public int getAmount() { + return events + .stream() + .mapToInt(GameEvent::getAmount) + .sum(); + } + + public int getLifeLostByPlayer(UUID playerID) { + return events + .stream() + .filter(ev -> ev.getTargetId().equals(playerID)) + .mapToInt(GameEvent::getAmount) + .sum(); + } + + public boolean isLifeLostByCombatDamage() { + return events.stream().anyMatch(LifeLostEvent::isCombatDamage); + } + + @Override + @Deprecated // events can store a diff value, so search it from events list instead + public UUID getTargetId() { + throw new IllegalStateException("Wrong code usage. Must search value from a getEvents list or use CardUtil.getEventTargets(event)"); + } + + @Override + @Deprecated // events can store a diff value, so search it from events list instead + public UUID getSourceId() { + throw new IllegalStateException("Wrong code usage. Must search value from a getEvents list."); + } + + public void addEvent(LifeLostEvent event) { + this.events.add(event); + } +} diff --git a/Mage/src/main/java/mage/game/events/LifeLostEvent.java b/Mage/src/main/java/mage/game/events/LifeLostEvent.java new file mode 100644 index 00000000000..30624654c77 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/LifeLostEvent.java @@ -0,0 +1,19 @@ +package mage.game.events; + +import mage.abilities.Ability; + +import java.util.UUID; + +/** + * @author jimga150 + */ +public class LifeLostEvent extends GameEvent{ + public LifeLostEvent(UUID playerId, Ability source, int amount, boolean atCombat){ + super(GameEvent.EventType.LOST_LIFE, + playerId, source, playerId, amount, atCombat); + } + + public boolean isCombatDamage() { + return flag; + } +} diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 1fc300ac559..05a650372f3 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -2175,8 +2175,9 @@ public abstract class PlayerImpl implements Player, Serializable { + (atCombat ? " at combat" : "") + CardUtil.getSourceLogName(game, " from ", needId, "", "")); } if (event.getAmount() > 0) { - game.fireEvent(new GameEvent(GameEvent.EventType.LOST_LIFE, - playerId, source, playerId, event.getAmount(), atCombat)); + LifeLostEvent lifeLostEvent = new LifeLostEvent(playerId, source, event.getAmount(), atCombat); + game.fireEvent(lifeLostEvent); + game.getState().addSimultaneousLifeLossEventToBatches(lifeLostEvent, game); } return event.getAmount(); }