Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add life lost event and batch event to implement Ob Nixilis, Captive Kingpin #11974

Merged
merged 12 commits into from
Mar 21, 2024
105 changes: 105 additions & 0 deletions Mage.Sets/src/mage/cards/o/ObNixilisCaptiveKingpin.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions Mage.Sets/src/mage/sets/MarchOfTheMachineTheAftermath.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ private MarchOfTheMachineTheAftermath() {
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));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
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 {

@Test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a details list from your comment about possible use cases can be useful here (just copy as comment).

IMG_0022

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Miss setStrictChooseMode before setstop/execute. It allows to check commands order and missing commands. Must be used all the time.

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);
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);
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);
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);
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");

activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{T}, Pay 1 life");

setStopAt(1, PhaseStep.END_TURN);
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);
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");

activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{B}, {T}");

setStopAt(1, PhaseStep.END_TURN);
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");

castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Blood Pact");

setStopAt(1, PhaseStep.END_TURN);
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);
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);
execute();

assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 0);
}

}

19 changes: 19 additions & 0 deletions Mage/src/main/java/mage/game/GameState.java
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,25 @@ public boolean hasSimultaneousEvents() {
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)
Expand Down
4 changes: 4 additions & 0 deletions Mage/src/main/java/mage/game/events/GameEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ combines all player damage events to a single batch (event) and split it per dam
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
Expand Down
69 changes: 69 additions & 0 deletions Mage/src/main/java/mage/game/events/LifeLostBatchEvent.java
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be wrong usage. Related card must use events list or use method like getLifeLoseByCombatDamage(isCombatDamage::Boolaean) > 0, not 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);
}
}
Loading
Loading