[MAT] implemented Ob Nixilis, Captive Kingpin, refactored life lose and batches events (#11974)

This commit is contained in:
jimga150 2024-03-21 11:53:45 -04:00 committed by GitHub
parent 0987e01f92
commit 50c75f05bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 424 additions and 2 deletions

View file

@ -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);
}
}

View file

@ -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", 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", 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("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", 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", 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)); cards.add(new SetCardInfo("Open the Way", 23, Rarity.RARE, mage.cards.o.OpenTheWay.class, NON_FULL_USE_VARIOUS));

View file

@ -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);
}
}

View file

@ -808,6 +808,25 @@ public class GameState implements Serializable, Copyable<GameState> {
return !simultaneousEvents.isEmpty(); 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) { public void addSimultaneousDamage(DamagedEvent damagedEvent, Game game) {
// Combine multiple damage events in the single event (batch) // Combine multiple damage events in the single event (batch)
// * per damage type (see GameEvent.DAMAGED_BATCH_FOR_PERMANENTS, GameEvent.DAMAGED_BATCH_FOR_PLAYERS) // * per damage type (see GameEvent.DAMAGED_BATCH_FOR_PERMANENTS, GameEvent.DAMAGED_BATCH_FOR_PLAYERS)

View file

@ -142,6 +142,10 @@ public class GameEvent implements Serializable {
amount amount of life loss amount amount of life loss
flag true = from combat damage - other from non combat damage 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, PLAY_LAND, LAND_PLAYED,
CREATURE_CHAMPIONED, CREATURE_CHAMPIONED,
/* CREATURE_CHAMPIONED /* CREATURE_CHAMPIONED

View file

@ -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<LifeLostEvent> {
private final Set<LifeLostEvent> events = new HashSet<>();
public LifeLostBatchEvent(LifeLostEvent event) {
super(EventType.LOST_LIFE_BATCH, null, null, null);
addEvent(event);
}
@Override
public Set<LifeLostEvent> getEvents() {
return events;
}
@Override
public Set<UUID> 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);
}
}

View file

@ -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;
}
}

View file

@ -2175,8 +2175,9 @@ public abstract class PlayerImpl implements Player, Serializable {
+ (atCombat ? " at combat" : "") + CardUtil.getSourceLogName(game, " from ", needId, "", "")); + (atCombat ? " at combat" : "") + CardUtil.getSourceLogName(game, " from ", needId, "", ""));
} }
if (event.getAmount() > 0) { if (event.getAmount() > 0) {
game.fireEvent(new GameEvent(GameEvent.EventType.LOST_LIFE, LifeLostEvent lifeLostEvent = new LifeLostEvent(playerId, source, event.getAmount(), atCombat);
playerId, source, playerId, event.getAmount(), atCombat)); game.fireEvent(lifeLostEvent);
game.getState().addSimultaneousLifeLossEventToBatches(lifeLostEvent, game);
} }
return event.getAmount(); return event.getAmount();
} }