Implement Damage Batch for Permanent event (#11841)

* implement [WHO] Donna Noble

* Changed trigger to DAMAGED_BATCH_FOR_PERMANENTS, check for need of separate targets

* fix short circuit operator

* simplify control path in paired damage trigger

* Initial commit, missing tests

* use CardUtil.getEventTargets

* Implement Donna Noble using DamagedBatchForOnePermanentEvent

* fix double-effect bug

* remove unnecessary custom effect

* Fix addSimultaneousDamage to avoid adding damage events to existing DamagedBatchForOnePlayerEvent instances when they shouldnt

* Add clarifying comment

* Incorporate batching of DAMAGED_BATCH_FOR_ONE_PERMANENT into if-else if tree to match new logic

* Add tests

* make ability inline

* Move DamageBatchTests

* Change batch events to take first event in constructor
This commit is contained in:
jimga150 2024-03-17 16:15:50 -04:00 committed by GitHub
parent b2aa8abba2
commit 67286aa1a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 365 additions and 12 deletions

View file

@ -0,0 +1,114 @@
package mage.cards.d;
import java.util.UUID;
import mage.MageInt;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.dynamicvalue.common.SavedDamageValue;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.constants.*;
import mage.abilities.keyword.SoulbondAbility;
import mage.abilities.keyword.DoctorsCompanionAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.game.Game;
import mage.game.events.DamagedBatchForOnePermanentEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.common.TargetOpponent;
import mage.util.CardUtil;
/**
*
* @author jimga150
*/
public final class DonnaNoble extends CardImpl {
public DonnaNoble(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.HUMAN);
this.power = new MageInt(2);
this.toughness = new MageInt(4);
// Soulbond
this.addAbility(new SoulbondAbility());
// Whenever Donna or a creature it's paired with is dealt damage, Donna deals that much damage to target opponent.
this.addAbility(new DonnaNobleTriggeredAbility());
// If Donna is paired with another creature and they are both dealt damage at the same time,
// the second ability triggers twice. (2023-10-13)
// Donna's ability triggers when either creature is dealt damage even if one or both were dealt lethal damage.
// (2023-10-13)
// Doctor's companion
this.addAbility(DoctorsCompanionAbility.getInstance());
}
private DonnaNoble(final DonnaNoble card) {
super(card);
}
@Override
public DonnaNoble copy() {
return new DonnaNoble(this);
}
}
// Based on DealtDamageToSourceTriggeredAbility, except this uses DamagedBatchForOnePermanentEvent,
// which batches all damage dealt at the same time on a permanent-by-permanent basis
class DonnaNobleTriggeredAbility extends TriggeredAbilityImpl {
DonnaNobleTriggeredAbility() {
super(Zone.BATTLEFIELD, new DamageTargetEffect(SavedDamageValue.MUCH));
this.addTarget(new TargetOpponent());
this.setTriggerPhrase("Whenever {this} or a creature it's paired with is dealt damage, ");
}
private DonnaNobleTriggeredAbility(final DonnaNobleTriggeredAbility ability) {
super(ability);
}
@Override
public DonnaNobleTriggeredAbility copy() {
return new DonnaNobleTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
DamagedBatchForOnePermanentEvent dEvent = (DamagedBatchForOnePermanentEvent) event;
// check if the permanent is Donna or its paired card
if (!CardUtil.getEventTargets(dEvent).contains(getSourceId())){
Permanent paired;
Permanent permanent = game.getPermanent(getSourceId());
if (permanent != null && permanent.getPairedCard() != null) {
paired = permanent.getPairedCard().getPermanent(game);
if (paired == null || paired.getPairedCard() == null || !paired.getPairedCard().equals(new MageObjectReference(permanent, game))) {
return false;
}
} else {
return false;
}
if (!CardUtil.getEventTargets(dEvent).contains(paired.getId())){
return false;
}
}
int damage = dEvent.getAmount();
if (damage < 1) {
return false;
}
this.getEffects().setValue("damage", damage);
return true;
}
}

View file

@ -72,6 +72,7 @@ public final class DoctorWho extends ExpansionSet {
cards.add(new SetCardInfo("Desolate Lighthouse", 271, Rarity.RARE, mage.cards.d.DesolateLighthouse.class));
cards.add(new SetCardInfo("Dinosaurs on a Spaceship", 122, Rarity.RARE, mage.cards.d.DinosaursOnASpaceship.class));
cards.add(new SetCardInfo("Displaced Dinosaurs", 100, Rarity.UNCOMMON, mage.cards.d.DisplacedDinosaurs.class));
cards.add(new SetCardInfo("Donna Noble", 82, Rarity.RARE, mage.cards.d.DonnaNoble.class));
cards.add(new SetCardInfo("Dragonskull Summit", 272, Rarity.RARE, mage.cards.d.DragonskullSummit.class));
cards.add(new SetCardInfo("Dreamroot Cascade", 273, Rarity.RARE, mage.cards.d.DreamrootCascade.class));
cards.add(new SetCardInfo("Drowned Catacomb", 274, Rarity.RARE, mage.cards.d.DrownedCatacomb.class));

View file

@ -0,0 +1,138 @@
package org.mage.test.cards.triggers.damage;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestCommander4Players;
/**
*
* @author jimga150
*/
public class DonnaNobleTests extends CardTestCommander4Players {
@Test
public void PairedCreature2To1Test() {
//Check that paired creature being dealt damage by 2 sources at the same time = 1 trigger with correct amount
addCard(Zone.BATTLEFIELD, playerA, "Donna Noble", 1); // Legendary Creature Human 2/4 {3}{R}
addCard(Zone.HAND, playerA, "Impervious Greatwurm", 1); // Creature Wurm 16/16 {7}{G}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 10);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Impervious Greatwurm", true);
//Yes, soul bond donna noble with IG
setChoice(playerA, "Yes");
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1); // Artifact Creature Construct 1/1 {0}
addCard(Zone.BATTLEFIELD, playerB, "Expedition Envoy", 1); // Creature Human Scout Ally 2/1 {W}
attack(5, playerA, "Impervious Greatwurm", playerB);
block(5, playerB, "Memnite", "Impervious Greatwurm");
block(5, playerB, "Expedition Envoy", "Impervious Greatwurm");
//Assign this much damage to the first blocking creature
setChoice(playerA, "X=1");
//Assign this much damage to the second blocking creature
setChoice(playerA, "X=1");
//Target this player with Donna Noble
addTarget(playerA, playerB);
setStopAt(5, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, currentGame.getStartingLife());
assertLife(playerB, currentGame.getStartingLife() - 3);
}
@Test
public void DonnaAndPairedBothDamagedSingleSourceTest() {
//Check that Donna and paired creature both damaged at the same time by one source = 2 triggers with correct amounts
addCard(Zone.BATTLEFIELD, playerA, "Donna Noble", 1); // Legendary Creature Human 2/4 {3}{R}
addCard(Zone.HAND, playerA, "Impervious Greatwurm", 1); // Creature Wurm 16/16 {7}{G}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 10);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Impervious Greatwurm", true);
//Yes, soul bond donna noble with IG
setChoice(playerA, "Yes");
// Kicker {R} (You may pay an additional {R} as you cast this spell.)
// Cinderclasm deals 1 damage to each creature. If it was kicked, it deals 2 damage to each creature instead.
addCard(Zone.HAND, playerA, "Cinderclasm", 1); // Instant {1}{R}
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cinderclasm", true);
//Yes, pay kicker for Cinderclasm
setChoice(playerA, "Yes");
//pick triggered ability starting with this string to enter the stack first
setChoice(playerA, "Whenever");
//Target this player with Donna Noble
addTarget(playerA, playerB);
//Target this player with Donna Noble (second trigger)
addTarget(playerA, playerB);
setStopAt(2, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, currentGame.getStartingLife());
assertLife(playerB, currentGame.getStartingLife() - 4);
}
@Test
public void DonnaAndPairedBothDamagedDiffSourceTest() {
//Check that Donna and paired creature both damaged at the same time by different sources = 2 triggers with correct amounts
addCard(Zone.BATTLEFIELD, playerA, "Donna Noble", 1); // Legendary Creature Human 2/4 {3}{R}
addCard(Zone.HAND, playerA, "Impervious Greatwurm", 1); // Creature Wurm 16/16 {7}{G}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 10);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Impervious Greatwurm", true);
//Yes, soul bond donna noble with IG
setChoice(playerA, "Yes");
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1); // Artifact Creature Construct 1/1 {0}
addCard(Zone.BATTLEFIELD, playerB, "Expedition Envoy", 1); // Creature Human Scout Ally 2/1 {W}
attack(4, playerB, "Memnite", playerA);
attack(4, playerB, "Expedition Envoy", playerA);
block(4, playerA, "Impervious Greatwurm", "Memnite");
block(4, playerA, "Donna Noble", "Expedition Envoy");
//pick triggered ability starting with this string to enter the stack first
setChoice(playerA, "Whenever");
//Target this player with Donna Noble
addTarget(playerA, playerB);
//Target this player with Donna Noble (second trigger)
addTarget(playerA, playerB);
setStopAt(4, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, currentGame.getStartingLife());
assertLife(playerB, currentGame.getStartingLife() - 3);
}
}

View file

@ -0,0 +1,71 @@
package org.mage.test.cards.triggers.delayed;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestCommander4Players;
/**
*
* @author jimga150
*/
public class DamagedBatchTests extends CardTestCommander4Players {
@Test
public void DamageBatchForOnePermanent2To1Test() {
//Check that one creature being dealt damage by 2 sources at the same time = 1 trigger of DAMAGED_BATCH_FOR_ONE_PERMANENT
addCard(Zone.BATTLEFIELD, playerA, "Donna Noble", 1); // Legendary Creature Human 2/4 {3}{R}
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1); // Artifact Creature Construct 1/1 {0}
addCard(Zone.BATTLEFIELD, playerB, "Expedition Envoy", 1); // Creature Human Scout Ally 2/1 {W}
attack(1, playerA, "Donna Noble", playerB);
block(1, playerB, "Memnite", "Donna Noble");
block(1, playerB, "Expedition Envoy", "Donna Noble");
//Assign this much damage to the first blocking creature
setChoice(playerA, "X=1");
//Target this player with Donna Noble
addTarget(playerA, playerB);
setStopAt(2, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, currentGame.getStartingLife());
assertLife(playerB, currentGame.getStartingLife() - 3);
}
@Test
public void DamageBatchForOnePermanent2EventTest() {
//Check that one creature being dealt damage at 2 different times (double strike in this case) = 2 triggers of DAMAGED_BATCH_FOR_ONE_PERMANENT
addCard(Zone.BATTLEFIELD, playerA, "Donna Noble", 1); // Legendary Creature Human 2/4 {3}{R}
addCard(Zone.BATTLEFIELD, playerB, "Adorned Pouncer", 1); // Creature Cat 1/1 {1}{W}
attack(1, playerA, "Donna Noble", playerB);
block(1, playerB, "Adorned Pouncer", "Donna Noble");
//Target this player with Donna Noble
addTarget(playerA, playerB);
//Target this player with Donna Noble (second trigger)
addTarget(playerA, playerB);
setStopAt(2, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, currentGame.getStartingLife());
assertLife(playerB, currentGame.getStartingLife() - 2);
}
}

View file

@ -812,15 +812,18 @@ public class GameState implements Serializable, Copyable<GameState> {
// Combine multiple damage events in the single event (batch)
// * per damage type (see GameEvent.DAMAGED_BATCH_FOR_PERMANENTS, GameEvent.DAMAGED_BATCH_FOR_PLAYERS)
// * per player (see GameEvent.DAMAGED_BATCH_FOR_ONE_PLAYER)
// * per permanent (see GameEvent.DAMAGED_BATCH_FOR_ONE_PERMANENT)
//
// Warning, one event can be stored in multiple batches,
// example: DAMAGED_BATCH_FOR_PLAYERS + DAMAGED_BATCH_FOR_ONE_PLAYER
boolean isPlayerDamage = damagedEvent instanceof DamagedPlayerEvent;
boolean isPermanentDamage = damagedEvent instanceof DamagedPermanentEvent;
// existing batch
boolean isDamageBatchUsed = false;
boolean isPlayerBatchUsed = false;
boolean isPermanentBatchUsed = false;
for (GameEvent event : simultaneousEvents) {
if (isPlayerDamage && event instanceof DamagedBatchForOnePlayerEvent) {
@ -831,6 +834,14 @@ public class GameState implements Serializable, Copyable<GameState> {
oldPlayerBatch.addEvent(damagedEvent);
isPlayerBatchUsed = true;
}
} else if (isPermanentDamage && event instanceof DamagedBatchForOnePermanentEvent) {
// per permanent
DamagedBatchForOnePermanentEvent oldPermanentBatch = (DamagedBatchForOnePermanentEvent) event;
if (oldPermanentBatch.getDamageClazz().isInstance(damagedEvent)
&& CardUtil.getEventTargets(event).contains(damagedEvent.getTargetId())) {
oldPermanentBatch.addEvent(damagedEvent);
isPermanentBatchUsed = true;
}
} else if ((event instanceof DamagedBatchEvent)
&& ((DamagedBatchEvent) event).getDamageClazz().isInstance(damagedEvent)) {
// per damage type
@ -842,6 +853,7 @@ public class GameState implements Serializable, Copyable<GameState> {
((DamagedBatchEvent) event).addEvent(damagedEvent);
isDamageBatchUsed = true;
}
}
// new batch
@ -849,8 +861,11 @@ public class GameState implements Serializable, Copyable<GameState> {
addSimultaneousEvent(DamagedBatchEvent.makeEvent(damagedEvent), game);
}
if (!isPlayerBatchUsed && isPlayerDamage) {
DamagedBatchEvent event = new DamagedBatchForOnePlayerEvent(damagedEvent.getTargetId());
event.addEvent(damagedEvent);
DamagedBatchEvent event = new DamagedBatchForOnePlayerEvent(damagedEvent);
addSimultaneousEvent(event, game);
}
if (!isPermanentBatchUsed && isPermanentDamage) {
DamagedBatchEvent event = new DamagedBatchForOnePermanentEvent(damagedEvent);
addSimultaneousEvent(event, game);
}
}

View file

@ -67,11 +67,9 @@ public abstract class DamagedBatchEvent extends GameEvent implements BatchGameEv
public static DamagedBatchEvent makeEvent(DamagedEvent damagedEvent) {
DamagedBatchEvent event;
if (damagedEvent instanceof DamagedPlayerEvent) {
event = new DamagedBatchForPlayersEvent();
event.addEvent(damagedEvent);
event = new DamagedBatchForPlayersEvent(damagedEvent);
} else if (damagedEvent instanceof DamagedPermanentEvent) {
event = new DamagedBatchForPermanentsEvent();
event.addEvent(damagedEvent);
event = new DamagedBatchForPermanentsEvent(damagedEvent);
} else {
throw new IllegalArgumentException("Wrong code usage. Unknown damage event for a new batch: " + damagedEvent.getClass().getName());
}

View file

@ -0,0 +1,10 @@
package mage.game.events;
public class DamagedBatchForOnePermanentEvent extends DamagedBatchEvent {
public DamagedBatchForOnePermanentEvent(DamagedEvent firstEvent) {
super(GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT, DamagedPermanentEvent.class);
addEvent(firstEvent);
setTargetId(firstEvent.getTargetId());
}
}

View file

@ -1,14 +1,13 @@
package mage.game.events;
import java.util.UUID;
/**
* @author Susucr
*/
public class DamagedBatchForOnePlayerEvent extends DamagedBatchEvent {
public DamagedBatchForOnePlayerEvent(UUID playerId) {
public DamagedBatchForOnePlayerEvent(DamagedEvent firstEvent) {
super(EventType.DAMAGED_BATCH_FOR_ONE_PLAYER, DamagedPlayerEvent.class);
this.setPlayerId(playerId);
setPlayerId(firstEvent.getPlayerId());
addEvent(firstEvent);
}
}

View file

@ -5,7 +5,8 @@ package mage.game.events;
*/
public class DamagedBatchForPermanentsEvent extends DamagedBatchEvent {
public DamagedBatchForPermanentsEvent() {
public DamagedBatchForPermanentsEvent(DamagedEvent firstEvent) {
super(EventType.DAMAGED_BATCH_FOR_PERMANENTS, DamagedPermanentEvent.class);
addEvent(firstEvent);
}
}

View file

@ -5,7 +5,8 @@ package mage.game.events;
*/
public class DamagedBatchForPlayersEvent extends DamagedBatchEvent {
public DamagedBatchForPlayersEvent() {
public DamagedBatchForPlayersEvent(DamagedEvent firstEvent) {
super(GameEvent.EventType.DAMAGED_BATCH_FOR_PLAYERS, DamagedPlayerEvent.class);
addEvent(firstEvent);
}
}

View file

@ -450,6 +450,11 @@ public class GameEvent implements Serializable {
*/
DAMAGED_BATCH_FOR_PERMANENTS,
/* DAMAGED_BATCH_FOR_ONE_PERMANENT
combines all permanent damage events to a single batch (event) and split it per damaged permanent
*/
DAMAGED_BATCH_FOR_ONE_PERMANENT,
DESTROY_PERMANENT,
/* DESTROY_PERMANENT_BY_LEGENDARY_RULE
targetId id of the permanent to destroy