From d645facdc08697c49a3717c94177fcecb1c73c21 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sat, 27 Apr 2024 17:34:59 +0200 Subject: [PATCH] rework PhantomReplacementEffect used by 7 Phantom cards (#12189) --- .../src/mage/cards/p/PhantomCentaur.java | 6 +- Mage.Sets/src/mage/cards/p/PhantomFlock.java | 6 +- .../src/mage/cards/p/PhantomNantuko.java | 8 +- .../src/mage/cards/p/PhantomNishoba.java | 6 +- Mage.Sets/src/mage/cards/p/PhantomNomad.java | 9 +- Mage.Sets/src/mage/cards/p/PhantomTiger.java | 6 +- Mage.Sets/src/mage/cards/p/PhantomWurm.java | 9 +- .../cards/single/tsp/PhantomWurmTest.java | 237 ++++++++++++++++++ .../effects/PhantomPreventionEffect.java | 83 ++++-- Mage/src/main/java/mage/game/GameState.java | 10 + .../DamagedBatchCouldHaveFiredEvent.java | 15 ++ .../main/java/mage/game/events/GameEvent.java | 6 + .../mage/game/permanent/PermanentImpl.java | 3 + .../java/mage/game/turn/CombatDamageStep.java | 7 +- .../main/java/mage/players/PlayerImpl.java | 17 +- 15 files changed, 366 insertions(+), 62 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tsp/PhantomWurmTest.java create mode 100644 Mage/src/main/java/mage/game/events/DamagedBatchCouldHaveFiredEvent.java diff --git a/Mage.Sets/src/mage/cards/p/PhantomCentaur.java b/Mage.Sets/src/mage/cards/p/PhantomCentaur.java index 50afe1aa4c9..18682c4b0f7 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomCentaur.java +++ b/Mage.Sets/src/mage/cards/p/PhantomCentaur.java @@ -1,7 +1,6 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; import mage.ObjectColor; import mage.abilities.common.EntersBattlefieldAbility; @@ -13,9 +12,10 @@ 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 java.util.UUID; + /** * @author LevelX2 */ @@ -36,7 +36,7 @@ public final class PhantomCentaur extends CardImpl { this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance(3)), "with three +1/+1 counters on it")); // If damage would be dealt to Phantom Centaur, prevent that damage. Remove a +1/+1 counter from Phantom Centaur. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PhantomPreventionEffect())); + this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); } private PhantomCentaur(final PhantomCentaur card) { diff --git a/Mage.Sets/src/mage/cards/p/PhantomFlock.java b/Mage.Sets/src/mage/cards/p/PhantomFlock.java index ca90d818fb5..ef667a25540 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomFlock.java +++ b/Mage.Sets/src/mage/cards/p/PhantomFlock.java @@ -1,7 +1,6 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; @@ -12,9 +11,10 @@ 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 java.util.UUID; + /** * @author emerald000 */ @@ -35,7 +35,7 @@ public final class PhantomFlock extends CardImpl { this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance(3)), "with three +1/+1 counters on it")); // If damage would be dealt to Phantom Flock, prevent that damage. Remove a +1/+1 counter from Phantom Flock. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PhantomPreventionEffect())); + this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); } private PhantomFlock(final PhantomFlock card) { diff --git a/Mage.Sets/src/mage/cards/p/PhantomNantuko.java b/Mage.Sets/src/mage/cards/p/PhantomNantuko.java index 47515f367db..073074502e8 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomNantuko.java +++ b/Mage.Sets/src/mage/cards/p/PhantomNantuko.java @@ -1,7 +1,6 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleActivatedAbility; @@ -17,14 +16,15 @@ import mage.constants.SubType; import mage.constants.Zone; import mage.counters.CounterType; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class PhantomNantuko extends CardImpl { public PhantomNantuko(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{G}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); this.subtype.add(SubType.INSECT); this.subtype.add(SubType.SPIRIT); @@ -36,7 +36,7 @@ public final class PhantomNantuko extends CardImpl { // Phantom Nantuko enters the battlefield with two +1/+1 counters on it. this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance(2), true), "with two +1/+1 counters on it")); // If damage would be dealt to Phantom Nantuko, prevent that damage. Remove a +1/+1 counter from Phantom Nantuko. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PhantomPreventionEffect())); + this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); // {tap}: Put a +1/+1 counter on Phantom Nantuko. this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.P1P1.createInstance()), new TapSourceCost())); } diff --git a/Mage.Sets/src/mage/cards/p/PhantomNishoba.java b/Mage.Sets/src/mage/cards/p/PhantomNishoba.java index 3b8d0277d95..a66ae3b4fd8 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomNishoba.java +++ b/Mage.Sets/src/mage/cards/p/PhantomNishoba.java @@ -1,7 +1,6 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; import mage.abilities.common.DealsDamageGainLifeSourceTriggeredAbility; import mage.abilities.common.EntersBattlefieldAbility; @@ -13,9 +12,10 @@ 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 java.util.UUID; + /** * @author fireshoes */ @@ -39,7 +39,7 @@ public final class PhantomNishoba extends CardImpl { this.addAbility(new DealsDamageGainLifeSourceTriggeredAbility()); // If damage would be dealt to Phantom Nishoba, prevent that damage. Remove a +1/+1 counter from Phantom Nishoba. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PhantomPreventionEffect())); + this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); } private PhantomNishoba(final PhantomNishoba card) { diff --git a/Mage.Sets/src/mage/cards/p/PhantomNomad.java b/Mage.Sets/src/mage/cards/p/PhantomNomad.java index c80a7084443..7052b8243c5 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomNomad.java +++ b/Mage.Sets/src/mage/cards/p/PhantomNomad.java @@ -1,7 +1,6 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; @@ -11,17 +10,17 @@ 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 java.util.UUID; + /** - * * @author LevelX2 */ public final class PhantomNomad extends CardImpl { public PhantomNomad(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{1}{W}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); this.subtype.add(SubType.SPIRIT); this.subtype.add(SubType.NOMAD); @@ -33,7 +32,7 @@ public final class PhantomNomad extends CardImpl { "with two +1/+1 counters on it")); // If damage would be dealt to Phantom Nomad, prevent that damage. Remove a +1/+1 counter from Phantom Nomad. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PhantomPreventionEffect())); + this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); } diff --git a/Mage.Sets/src/mage/cards/p/PhantomTiger.java b/Mage.Sets/src/mage/cards/p/PhantomTiger.java index d32ce47e686..1521811f679 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomTiger.java +++ b/Mage.Sets/src/mage/cards/p/PhantomTiger.java @@ -1,7 +1,6 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; @@ -11,9 +10,10 @@ 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 java.util.UUID; + /** * @author Temba */ @@ -30,7 +30,7 @@ public final class PhantomTiger extends CardImpl { this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)), "with two +1/+1 counters on it")); // If damage would be dealt to Phantom Tiger, prevent that damage. Remove a +1/+1 counter from Phantom Tiger. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PhantomPreventionEffect())); + this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); } private PhantomTiger(final PhantomTiger card) { diff --git a/Mage.Sets/src/mage/cards/p/PhantomWurm.java b/Mage.Sets/src/mage/cards/p/PhantomWurm.java index 94cdf1c08e9..291b1b2367f 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomWurm.java +++ b/Mage.Sets/src/mage/cards/p/PhantomWurm.java @@ -1,7 +1,6 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; @@ -11,17 +10,17 @@ 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 java.util.UUID; + /** - * * @author LevelX2 */ public final class PhantomWurm extends CardImpl { public PhantomWurm(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{4}{G}{G}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{G}{G}"); this.subtype.add(SubType.WURM); this.subtype.add(SubType.SPIRIT); @@ -33,7 +32,7 @@ public final class PhantomWurm extends CardImpl { "with four +1/+1 counters on it")); // If damage would be dealt to Phantom Wurm, prevent that damage. Remove a +1/+1 counter from Phantom Wurm. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PhantomPreventionEffect())); + this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); } private PhantomWurm(final PhantomWurm card) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tsp/PhantomWurmTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tsp/PhantomWurmTest.java new file mode 100644 index 00000000000..0e64acdd57b --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tsp/PhantomWurmTest.java @@ -0,0 +1,237 @@ +package org.mage.test.cards.single.tsp; + +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 PhantomWurmTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.p.PhantomWurm Phantom Wurm} {4}{G}{G} + * Creature — Wurm Spirit + * Phantom Wurm enters the battlefield with four +1/+1 counters on it. + * If damage would be dealt to Phantom Wurm, prevent that damage. Remove a +1/+1 counter from Phantom Wurm. + * 2/0 + */ + private static final String wurm = "Phantom Wurm"; + + @Test + public void test_DoubleBlocked() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, wurm); + addCard(Zone.BATTLEFIELD, playerB, "Memnite"); + addCard(Zone.BATTLEFIELD, playerB, "Eager Cadet"); + + attack(1, playerA, wurm, playerB); + block(1, playerB, "Memnite", wurm); + block(1, playerB, "Eager Cadet", wurm); + + setChoice(playerA, "X=1"); // damage assignment + setChoice(playerA, "X=3"); // damage assignment + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerB, "Memnite", 1); + assertGraveyardCount(playerB, "Eager Cadet", 1); + assertDamageReceived(playerA, wurm, 0); // all is prevented + assertCounterCount(wurm, CounterType.P1P1, 4 - 1); + } + + @Test + public void test_BlockedByAnotherPhantom() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, wurm); + addCard(Zone.BATTLEFIELD, playerB, "Phantom Nomad"); + + attack(1, playerA, wurm, playerB); + block(1, playerB, "Phantom Nomad", wurm); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertDamageReceived(playerA, wurm, 0); // all is prevented + assertCounterCount(wurm, CounterType.P1P1, 4 - 1); + assertDamageReceived(playerB, "Phantom Nomad", 0); // all is prevented + assertCounterCount("Phantom Nomad", CounterType.P1P1, 2 - 1); + } + + @Test + public void test_BlockedByAnotherPhantom_ThenBolt() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, wurm); + addCard(Zone.BATTLEFIELD, playerB, "Phantom Nomad"); + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + addCard(Zone.HAND, playerB, "Lightning Bolt"); + + attack(1, playerA, wurm, playerB); + block(1, playerB, "Phantom Nomad", wurm); + + castSpell(1, PhaseStep.COMBAT_DAMAGE, playerB, "Lightning Bolt", wurm); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertDamageReceived(playerA, wurm, 0); // all is prevented + assertCounterCount(wurm, CounterType.P1P1, 4 - 2); + assertDamageReceived(playerB, "Phantom Nomad", 0); // all is prevented + assertCounterCount("Phantom Nomad", CounterType.P1P1, 2 - 1); + } + + @Test + public void test_DoubleStrike() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, wurm); + addCard(Zone.BATTLEFIELD, playerB, "Adorned Pouncer"); // 1/1 double strike + + attack(1, playerA, wurm, playerB); + block(1, playerB, "Adorned Pouncer", wurm); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerB, "Adorned Pouncer", 1); + assertDamageReceived(playerA, wurm, 0); // all is prevented + assertCounterCount(wurm, CounterType.P1P1, 4 - 2); + } + + @Test + public void test_DoubleBlocked_OneFirstStrikeOneNormal() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, wurm); + addCard(Zone.BATTLEFIELD, playerB, "Memnite"); + addCard(Zone.BATTLEFIELD, playerB, "Goblin Striker", 1); // 1/1 first strike haste + + attack(1, playerA, wurm, playerB); + block(1, playerB, "Memnite", wurm); + block(1, playerB, "Goblin Striker", wurm); + + setChoice(playerA, "X=1"); // damage assignment + setChoice(playerA, "X=3"); // damage assignment + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerB, "Memnite", 1); + assertGraveyardCount(playerB, "Goblin Striker", 1); + assertDamageReceived(playerA, wurm, 0); // all is prevented + assertCounterCount(wurm, CounterType.P1P1, 4 - 2); + } + + @Test + public void test_DoubleBlocked_TwoFirstStrike() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, wurm); + addCard(Zone.BATTLEFIELD, playerB, "Boros Recruit"); // 1/1 first strike + addCard(Zone.BATTLEFIELD, playerB, "Goblin Striker", 1); // 1/1 first strike haste + + attack(1, playerA, wurm, playerB); + block(1, playerB, "Boros Recruit", wurm); + block(1, playerB, "Goblin Striker", wurm); + + setChoice(playerA, "X=1"); // damage assignment + setChoice(playerA, "X=3"); // damage assignment + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerB, "Boros Recruit", 1); + assertGraveyardCount(playerB, "Goblin Striker", 1); + assertDamageReceived(playerA, wurm, 0); // all is prevented + assertCounterCount(wurm, CounterType.P1P1, 4 - 1); + } + + @Test + public void test_Blocked_ThenBolt() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, wurm); + addCard(Zone.BATTLEFIELD, playerB, "Memnite"); + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + addCard(Zone.HAND, playerB, "Lightning Bolt"); + + attack(1, playerA, wurm, playerB); + block(1, playerB, "Memnite", wurm); + + castSpell(1, PhaseStep.COMBAT_DAMAGE, playerB, "Lightning Bolt", wurm); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerB, "Memnite", 1); + assertDamageReceived(playerA, wurm, 0); // all is prevented + assertCounterCount(wurm, CounterType.P1P1, 4 - 2); + } + + @Test + public void test_Blocked_FirstStrike_ThenBolt() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, wurm); + addCard(Zone.BATTLEFIELD, playerB, "Boros Recruit"); + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + addCard(Zone.HAND, playerB, "Lightning Bolt"); + + attack(1, playerA, wurm, playerB); + block(1, playerB, "Boros Recruit", wurm); + + castSpell(1, PhaseStep.FIRST_COMBAT_DAMAGE, playerB, "Lightning Bolt", wurm); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerB, "Boros Recruit", 1); + assertDamageReceived(playerA, wurm, 0); // all is prevented + assertCounterCount(wurm, CounterType.P1P1, 4 - 2); + } + + @Test + public void test_Simultanous_NonCombat() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, wurm); + addCard(Zone.BATTLEFIELD, playerA, "Memnite"); + addCard(Zone.BATTLEFIELD, playerA, "Boros Recruit"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + addCard(Zone.HAND, playerA, "Band Together"); // Up to two target creatures you control each deal damage equal to their power to another target creature. + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Band Together"); + addTarget(playerA, "Memnite^Boros Recruit"); + addTarget(playerA, wurm); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertDamageReceived(playerA, wurm, 0); // all is prevented + assertCounterCount(wurm, CounterType.P1P1, 4 - 1); + } + + @Test + public void test_Bolt_ThenBolt() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, wurm); + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 2); + addCard(Zone.HAND, playerB, "Lightning Bolt", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", wurm); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", wurm); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertDamageReceived(playerA, wurm, 0); // all is prevented + assertCounterCount(wurm, CounterType.P1P1, 4 - 2); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/PhantomPreventionEffect.java b/Mage/src/main/java/mage/abilities/effects/PhantomPreventionEffect.java index 8c090e0c825..5eb8a35de7a 100644 --- a/Mage/src/main/java/mage/abilities/effects/PhantomPreventionEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/PhantomPreventionEffect.java @@ -1,22 +1,28 @@ package mage.abilities.effects; +import mage.MageObjectReference; import mage.abilities.Ability; import mage.constants.Duration; -import mage.constants.PhaseStep; +import mage.constants.WatcherScope; import mage.counters.CounterType; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; -import mage.game.turn.Step; +import mage.watchers.Watcher; + +import java.util.HashSet; +import java.util.Set; /** - * Created by IGOUDT on 22-3-2017. + * This requires to add the PhantomPreventionWatcher + * + * @author Susucr */ public class PhantomPreventionEffect extends PreventionEffectImpl { - // remember turn and phase step to check if counter in this step was already removed - private int turn = 0; - private Step combatPhaseStep = null; + public static PhantomPreventionWatcher createWatcher() { + return new PhantomPreventionWatcher(); + } public PhantomPreventionEffect() { super(Duration.WhileOnBattlefield); @@ -25,8 +31,6 @@ public class PhantomPreventionEffect extends PreventionEffectImpl { protected PhantomPreventionEffect(final PhantomPreventionEffect effect) { super(effect); - this.turn = effect.turn; - this.combatPhaseStep = effect.combatPhaseStep; } @Override @@ -37,27 +41,19 @@ public class PhantomPreventionEffect extends PreventionEffectImpl { @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { preventDamageAction(event, source, game); - Permanent permanent = game.getPermanent(source.getSourceId()); - if (permanent != null) { - boolean removeCounter = true; - // check if in the same combat damage step already a counter was removed - if (game.getTurn().getPhase().getStep().getType() == PhaseStep.COMBAT_DAMAGE) { - if (game.getTurnNum() == turn - && game.getTurn().getStep().equals(combatPhaseStep)) { - removeCounter = false; - } else { - turn = game.getTurnNum(); - combatPhaseStep = game.getTurn().getStep(); + PhantomPreventionWatcher watcher = game.getState().getWatcher(PhantomPreventionWatcher.class); + if (permanent != null && watcher != null) { + MageObjectReference mor = new MageObjectReference(source.getId(), source.getSourceObjectZoneChangeCounter(), game); + if (!watcher.hadMORCounterRemovedThisBatch(mor)) { + watcher.addMOR(mor); + if (permanent.getCounters(game).containsKey(CounterType.P1P1)) { + StringBuilder sb = new StringBuilder(permanent.getName()).append(": "); + permanent.removeCounters(CounterType.P1P1.createInstance(), source, game); + sb.append("Removed a +1/+1 counter "); + game.informPlayers(sb.toString()); } } - - if (removeCounter && permanent.getCounters(game).containsKey(CounterType.P1P1)) { - StringBuilder sb = new StringBuilder(permanent.getName()).append(": "); - permanent.removeCounters(CounterType.P1P1.createInstance(), source, game); - sb.append("Removed a +1/+1 counter "); - game.informPlayers(sb.toString()); - } } return false; @@ -72,5 +68,38 @@ public class PhantomPreventionEffect extends PreventionEffectImpl { } return false; } - } + +class PhantomPreventionWatcher extends Watcher { + + // We keep a very short-lived set of which PhantomPreventionEffect caused + // +1/+1 to get removed during the current damage batch. + private final Set morRemovedCounterThisDamageBatch = new HashSet<>(); + + PhantomPreventionWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + // This watcher resets every time a Damage Batch could have fired (even if all damage was prevented) + if (event.getType() != GameEvent.EventType.DAMAGED_BATCH_COULD_HAVE_FIRED) { + return; + } + morRemovedCounterThisDamageBatch.clear(); + } + + @Override + public void reset() { + super.reset(); + morRemovedCounterThisDamageBatch.clear(); + } + + boolean hadMORCounterRemovedThisBatch(MageObjectReference mor) { + return morRemovedCounterThisDamageBatch.contains(mor); + } + + void addMOR(MageObjectReference mor) { + morRemovedCounterThisDamageBatch.add(mor); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 2e328147438..8b00464843e 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -818,6 +818,16 @@ public class GameState implements Serializable, Copyable { return !simultaneousEvents.isEmpty(); } + // There might be no damage dealt, but we want to fire that damage (in a batch) could have been dealt. + public void addBatchDamageCouldHaveBeenFired(boolean combat, Game game) { + for (GameEvent event : simultaneousEvents) { + if (event instanceof DamagedBatchCouldHaveFiredEvent && event.getFlag() == combat) { + return; + } + } + addSimultaneousEvent(new DamagedBatchCouldHaveFiredEvent(combat), game); + } + public void addSimultaneousDamage(DamagedEvent damagedEvent, Game game) { // Combine multiple damage events in the single event (batch) // Note: one event can be stored in multiple batches diff --git a/Mage/src/main/java/mage/game/events/DamagedBatchCouldHaveFiredEvent.java b/Mage/src/main/java/mage/game/events/DamagedBatchCouldHaveFiredEvent.java new file mode 100644 index 00000000000..6a2ecdcaae3 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/DamagedBatchCouldHaveFiredEvent.java @@ -0,0 +1,15 @@ +package mage.game.events; + +/** + * Does not contain any info on damage events, and can fire even when all damage is prevented. + * Fire any time a DAMAGED_BATCH_FOR_ALL could have fired (combat & noncombat). + * It is not a batch event (doesn't contain sub events), the name is a little ambiguous. + * + * @author Susucr + */ +public class DamagedBatchCouldHaveFiredEvent extends GameEvent { + + public DamagedBatchCouldHaveFiredEvent(boolean combat) { + super(EventType.DAMAGED_BATCH_COULD_HAVE_FIRED, null, null, null, 0, combat); + } +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index abea9ecbc75..35d192ae231 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -129,6 +129,12 @@ public class GameEvent implements Serializable { includes all damage events, both permanent damage and player damage, in single batch event */ DAMAGED_BATCH_FOR_ALL, + /* DAMAGED_BATCH_FIRED + * Does not contain any info on damage events, and can fire even when all damage is prevented. + * Fire any time a DAMAGED_BATCH_FOR_ALL could have fired (combat & noncombat). + * It is not a batch event (doesn't contain sub events), the name is a little ambiguous. + */ + DAMAGED_BATCH_COULD_HAVE_FIRED, /* DAMAGE_CAUSES_LIFE_LOSS, targetId the id of the damaged player diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index 59b53c6a5d7..d8407779588 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -1024,6 +1024,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { } DamageEvent event = new DamagePermanentEvent(objectId, attackerId, controllerId, damageAmount, preventable, combat); event.setAppliedEffects(appliedEffects); + // Even if no damage was dealt, some watchers would need a reset next time actions are processed. + // For instance PhantomPreventionWatcher used by the [[Phantom Wurm]] type of replacement effect. + game.getState().addBatchDamageCouldHaveBeenFired(combat, game); if (game.replaceEvent(event)) { return 0; } diff --git a/Mage/src/main/java/mage/game/turn/CombatDamageStep.java b/Mage/src/main/java/mage/game/turn/CombatDamageStep.java index afd091d5189..967397d45ea 100644 --- a/Mage/src/main/java/mage/game/turn/CombatDamageStep.java +++ b/Mage/src/main/java/mage/game/turn/CombatDamageStep.java @@ -1,13 +1,13 @@ package mage.game.turn; -import java.util.UUID; - import mage.constants.PhaseStep; import mage.game.Game; import mage.game.combat.CombatGroup; import mage.game.events.GameEvent; import mage.game.events.GameEvent.EventType; +import java.util.UUID; + /** * @author BetaSteward_at_googlemail.com */ @@ -65,6 +65,9 @@ public class CombatDamageStep extends Step { for (CombatGroup group : game.getCombat().getBlockingGroups()) { group.applyDamage(game); } + + // Even if no damage was dealt, some watchers need a reset. For instance PhantomPreventionWatcher. + game.getState().addBatchDamageCouldHaveBeenFired(true, game); // Must fire damage batch events now, before SBA (https://github.com/magefree/mage/issues/9129) game.getState().handleSimultaneousEvent(game); } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index dfdd5e977c3..939e8a2ae70 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -2255,7 +2255,7 @@ public abstract class PlayerImpl implements Player, Serializable { return doDamage(damage, attackerId, source, game, combatDamage, preventable, appliedEffects); } - private int doDamage(int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List appliedEffects) { + private int doDamage(int damage, UUID attackerId, Ability source, Game game, boolean combat, boolean preventable, List appliedEffects) { if (!this.isInGame()) { return 0; } @@ -2263,8 +2263,11 @@ public abstract class PlayerImpl implements Player, Serializable { if (damage < 1) { return 0; } - DamageEvent event = new DamagePlayerEvent(playerId, attackerId, playerId, damage, preventable, combatDamage); + DamageEvent event = new DamagePlayerEvent(playerId, attackerId, playerId, damage, preventable, combat); event.setAppliedEffects(appliedEffects); + // Even if no damage was dealt, some watchers would need a reset next time actions are processed. + // For instance PhantomPreventionWatcher used by the [[Phantom Wurm]] type of replacement effect. + game.getState().addBatchDamageCouldHaveBeenFired(combat, game); if (game.replaceEvent(event)) { return 0; } @@ -2300,20 +2303,20 @@ public abstract class PlayerImpl implements Player, Serializable { addCounters(CounterType.POISON.createInstance(actualDamage), sourceControllerId, source, game); } else { GameEvent damageToLifeLossEvent = new GameEvent(GameEvent.EventType.DAMAGE_CAUSES_LIFE_LOSS, - playerId, source, playerId, actualDamage, combatDamage); + playerId, source, playerId, actualDamage, combat); if (!game.replaceEvent(damageToLifeLossEvent)) { - this.loseLife(damageToLifeLossEvent.getAmount(), game, source, combatDamage, attackerId); + this.loseLife(damageToLifeLossEvent.getAmount(), game, source, combat, attackerId); } } if (sourceAbilities != null && sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) { - if (combatDamage) { + if (combat) { game.getPermanent(attackerId).markLifelink(actualDamage); } else { Player player = game.getPlayer(sourceControllerId); player.gainLife(actualDamage, game, source); } } - if (combatDamage && sourceAbilities != null && sourceAbilities.containsClass(ToxicAbility.class)) { + if (combat && sourceAbilities != null && sourceAbilities.containsClass(ToxicAbility.class)) { int countersToAdd = CardUtil .castStream(sourceAbilities.stream(), ToxicAbility.class) .mapToInt(ToxicAbility::getAmount) @@ -2325,7 +2328,7 @@ public abstract class PlayerImpl implements Player, Serializable { Player player = game.getPlayer(sourceControllerId); new SquirrelToken().putOntoBattlefield(actualDamage, game, source, player.getId()); } - DamagedEvent damagedEvent = new DamagedPlayerEvent(playerId, attackerId, playerId, actualDamage, combatDamage); + DamagedEvent damagedEvent = new DamagedPlayerEvent(playerId, attackerId, playerId, actualDamage, combat); game.fireEvent(damagedEvent); game.getState().addSimultaneousDamage(damagedEvent, game); return actualDamage;