From 367a1fd189c5128892ce79e6e30c49e887a14a3e Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 1 May 2019 12:49:19 +0400 Subject: [PATCH] Added ConditionalPreventionEffect to support prevention effects with conditions (#5738) --- .../src/mage/cards/g/GideonBlackblade.java | 5 +- .../continuous/ConditionalPreventionTest.java | 137 +++++++++++++++++ .../continuous/GideonBlackbladeTest.java | 39 +++++ .../ConditionalContinuousEffect.java | 27 +++- .../ConditionalPreventionEffect.java | 140 ++++++++++++++++++ .../effects/ContinuousEffectImpl.java | 8 - .../abilities/effects/ContinuousEffects.java | 2 + 7 files changed, 340 insertions(+), 18 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/continuous/ConditionalPreventionTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/continuous/GideonBlackbladeTest.java create mode 100644 Mage/src/main/java/mage/abilities/decorator/ConditionalPreventionEffect.java diff --git a/Mage.Sets/src/mage/cards/g/GideonBlackblade.java b/Mage.Sets/src/mage/cards/g/GideonBlackblade.java index d1f44563d36..f57cd82e970 100644 --- a/Mage.Sets/src/mage/cards/g/GideonBlackblade.java +++ b/Mage.Sets/src/mage/cards/g/GideonBlackblade.java @@ -7,6 +7,7 @@ import mage.abilities.common.PlaneswalkerEntersWithLoyaltyCountersAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.MyTurnCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.decorator.ConditionalPreventionEffect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ExileTargetEffect; import mage.abilities.effects.common.PreventAllDamageToSourceEffect; @@ -61,7 +62,7 @@ public final class GideonBlackblade extends CardImpl { ))); // Prevent all damage that would be dealt to Gideon Blackblade during your turn. - this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect( + this.addAbility(new SimpleStaticAbility(new ConditionalPreventionEffect( new PreventAllDamageToSourceEffect(Duration.WhileOnBattlefield), MyTurnCondition.instance, "Prevent all damage that would be dealt to {this} during your turn." ))); @@ -96,7 +97,7 @@ class GideonBlackbladeToken extends TokenImpl { subtype.add(SubType.SOLDIER); power = new MageInt(4); toughness = new MageInt(4); - addAbility(new SimpleStaticAbility(new PreventAllDamageToSourceEffect(Duration.WhileOnBattlefield))); + this.addAbility(IndestructibleAbility.getInstance()); } private GideonBlackbladeToken(final GideonBlackbladeToken token) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/ConditionalPreventionTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/ConditionalPreventionTest.java new file mode 100644 index 00000000000..42c8b0bb6f7 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/ConditionalPreventionTest.java @@ -0,0 +1,137 @@ +package org.mage.test.cards.continuous; + +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.MyTurnCondition; +import mage.abilities.condition.common.NotMyTurnCondition; +import mage.abilities.decorator.ConditionalPreventionEffect; +import mage.abilities.effects.common.PreventAllDamageToAllEffect; +import mage.abilities.effects.common.PreventAllDamageToPlayersEffect; +import mage.constants.Duration; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class ConditionalPreventionTest extends CardTestPlayerBase { + + // conditional effects go to layered, but there are prevention effects list too + + @Test + public void test_NotPreventDamage() { + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertHandCount(playerA, "Lightning Bolt", 0); + } + + @Test + public void test_PreventDamageNormal() { + addCustomCardWithAbility("prevent", playerA, new SimpleStaticAbility(new PreventAllDamageToAllEffect(Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT))); + + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Balduvian Bears", 1); + assertHandCount(playerA, "Lightning Bolt", 0); + } + + @Test + public void test_PreventDamageConditionalActive() { + addCustomCardWithAbility("prevent", playerA, new SimpleStaticAbility( + new ConditionalPreventionEffect( + new PreventAllDamageToAllEffect(Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT), + MyTurnCondition.instance, + "" + ) + )); + + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Balduvian Bears", 1); + assertHandCount(playerA, "Lightning Bolt", 0); + } + + @Test + public void test_PreventDamageConditionalNotActive() { + addCustomCardWithAbility("prevent", playerA, new SimpleStaticAbility( + new ConditionalPreventionEffect( + new PreventAllDamageToAllEffect(Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT), + NotMyTurnCondition.instance, + "" + ) + )); + + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertHandCount(playerA, "Lightning Bolt", 0); + } + + @Test + public void test_PreventDamageConditionalNotActiveWithOtherEffect() { + addCustomCardWithAbility("prevent", playerA, new SimpleStaticAbility( + new ConditionalPreventionEffect( + new PreventAllDamageToAllEffect(Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT), + new PreventAllDamageToPlayersEffect(Duration.WhileOnBattlefield, false), + NotMyTurnCondition.instance, + "" + ) + )); + + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.HAND, playerA, "Lightning Bolt", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Balduvian Bears"); // will prevent + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA); // will not prevent + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Balduvian Bears", 0); // not prevented, dies + assertLife(playerA, 20); // prevented, no damage + assertHandCount(playerA, "Lightning Bolt", 0); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/GideonBlackbladeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/GideonBlackbladeTest.java new file mode 100644 index 00000000000..9f1baf27a7f --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/GideonBlackbladeTest.java @@ -0,0 +1,39 @@ +package org.mage.test.cards.continuous; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class GideonBlackbladeTest extends CardTestPlayerBase { + + // Gideon Blackblade L4 + // As long as it's your turn, Gideon Blackblade is a 4/4 Human Soldier creature with indestructible that's still a planeswalker. + // Prevent all damage that would be dealt to Gideon Blackblade during your turn. + + @Test + public void test_PreventDamageToGideonOnYourTurn() { + addCard(Zone.BATTLEFIELD, playerA, "Gideon Blackblade"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.HAND, playerA, "Lightning Bolt", 2); + + checkPT("turn 1 before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gideon Blackblade", 4, 4); + castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, "Lightning Bolt", "Gideon Blackblade"); + checkPT("turn 1 after", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Gideon Blackblade", 4, 4); + checkPermanentCounters("turn 1 after", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Gideon Blackblade", CounterType.LOYALTY, 4); + + checkPT("turn 2 before", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Gideon Blackblade", 0, 0); + castSpell(2, PhaseStep.BEGIN_COMBAT, playerA, "Lightning Bolt", "Gideon Blackblade"); + checkPT("turn 2 after", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, "Gideon Blackblade", 0, 0); + checkPermanentCounters("turn 2 after", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, "Gideon Blackblade", CounterType.LOYALTY, 4 - 3); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } +} diff --git a/Mage/src/main/java/mage/abilities/decorator/ConditionalContinuousEffect.java b/Mage/src/main/java/mage/abilities/decorator/ConditionalContinuousEffect.java index 1ce5392da33..254f20dd03e 100644 --- a/Mage/src/main/java/mage/abilities/decorator/ConditionalContinuousEffect.java +++ b/Mage/src/main/java/mage/abilities/decorator/ConditionalContinuousEffect.java @@ -1,9 +1,5 @@ package mage.abilities.decorator; -import java.util.EnumSet; -import java.util.List; -import java.util.Set; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.Mode; import mage.abilities.condition.Condition; @@ -11,11 +7,14 @@ import mage.abilities.condition.FixedCondition; import mage.abilities.condition.LockedInCondition; import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.ContinuousEffectImpl; -import mage.constants.DependencyType; -import mage.constants.Duration; -import mage.constants.Layer; -import mage.constants.SubLayer; +import mage.constants.*; import mage.game.Game; +import org.junit.Assert; + +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; /** * Adds condition to {@link ContinuousEffect}. Acts as decorator. @@ -48,6 +47,17 @@ public class ConditionalContinuousEffect extends ContinuousEffectImpl { this.otherwiseEffect = otherwiseEffect; this.baseCondition = condition; this.staticText = text; + + // checks for compatibility + if (effect != null && !effect.getEffectType().equals(EffectType.CONTINUOUS)) { + Assert.fail("ConditionalContinuousEffect supports only " + EffectType.CONTINUOUS.toString() + " but found " + effect.getEffectType().toString()); + } + if (otherwiseEffect != null && !otherwiseEffect.getEffectType().equals(EffectType.CONTINUOUS)) { + Assert.fail("ConditionalContinuousEffect supports only " + EffectType.CONTINUOUS.toString() + " but found " + effect.getEffectType().toString()); + } + if (effect != null && otherwiseEffect != null && !effect.getEffectType().equals(otherwiseEffect.getEffectType())) { + Assert.fail("ConditionalContinuousEffect must be same but found " + effect.getEffectType().toString() + " and " + otherwiseEffect.getEffectType().toString()); + } } public ConditionalContinuousEffect(final ConditionalContinuousEffect effect) { @@ -68,6 +78,7 @@ public class ConditionalContinuousEffect extends ContinuousEffectImpl { @Override public void init(Ability source, Game game) { + super.init(source, game); if (baseCondition instanceof LockedInCondition) { condition = new FixedCondition(((LockedInCondition) baseCondition).getBaseCondition().apply(game, source)); } else { diff --git a/Mage/src/main/java/mage/abilities/decorator/ConditionalPreventionEffect.java b/Mage/src/main/java/mage/abilities/decorator/ConditionalPreventionEffect.java new file mode 100644 index 00000000000..ec256f06dc0 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/decorator/ConditionalPreventionEffect.java @@ -0,0 +1,140 @@ +package mage.abilities.decorator; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.condition.Condition; +import mage.abilities.condition.FixedCondition; +import mage.abilities.condition.LockedInCondition; +import mage.abilities.effects.PreventionEffect; +import mage.abilities.effects.PreventionEffectImpl; +import mage.constants.Duration; +import mage.game.Game; +import mage.game.events.GameEvent; + +/** + * @author JayDi85 + */ +public class ConditionalPreventionEffect extends PreventionEffectImpl { + + protected PreventionEffect effect; + protected PreventionEffect otherwiseEffect; + protected Condition baseCondition; + protected Condition condition; + protected boolean conditionState; + protected boolean initDone = false; + + public ConditionalPreventionEffect(PreventionEffect effect, Condition condition, String text) { + this(effect, null, condition, text); + } + + /** + * Only use this if both effects have the same layers + * + * @param effect + * @param otherwiseEffect + * @param condition + * @param text + */ + public ConditionalPreventionEffect(PreventionEffect effect, PreventionEffect otherwiseEffect, Condition condition, String text) { + super(effect.getDuration()); + this.effect = effect; + this.otherwiseEffect = otherwiseEffect; + this.baseCondition = condition; + this.staticText = text; + } + + public ConditionalPreventionEffect(final ConditionalPreventionEffect effect) { + super(effect); + this.effect = (PreventionEffect) effect.effect.copy(); + if (effect.otherwiseEffect != null) { + this.otherwiseEffect = (PreventionEffect) effect.otherwiseEffect.copy(); + } + this.condition = effect.condition; // TODO: checks conditional copy -- it's can be usefull for memory leaks fix? + this.conditionState = effect.conditionState; + this.baseCondition = effect.baseCondition; + this.initDone = effect.initDone; + } + + @Override + public boolean isDiscarded() { + return this.discarded || effect.isDiscarded() || (otherwiseEffect != null && otherwiseEffect.isDiscarded()); + } + + @Override + public void init(Ability source, Game game) { + super.init(source, game); + if (baseCondition instanceof LockedInCondition) { + condition = new FixedCondition(((LockedInCondition) baseCondition).getBaseCondition().apply(game, source)); + } else { + condition = baseCondition; + } + effect.setTargetPointer(this.targetPointer); + effect.init(source, game); + if (otherwiseEffect != null) { + otherwiseEffect.setTargetPointer(this.targetPointer); + otherwiseEffect.init(source, game); + } + initDone = true; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + if (conditionState) { + effect.setTargetPointer(this.targetPointer); + return effect.replaceEvent(event, source, game); + } else if (otherwiseEffect != null) { + otherwiseEffect.setTargetPointer(this.targetPointer); + return otherwiseEffect.replaceEvent(event, source, game); + } + + if (!conditionState && effect.getDuration() == Duration.OneUse) { + used = true; + } + if (!conditionState && effect.getDuration() == Duration.Custom) { + this.discard(); + } + + return false; + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return effect.checksEventType(event, game) + || (otherwiseEffect != null && otherwiseEffect.checksEventType(event, game)); + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + if (!initDone) { // if simpleStaticAbility, init won't be called + init(source, game); + } + conditionState = condition.apply(game, source); + if (conditionState) { + effect.setTargetPointer(this.targetPointer); + return effect.applies(event, source, game); + } else if (otherwiseEffect != null) { + otherwiseEffect.setTargetPointer(this.targetPointer); + return otherwiseEffect.applies(event, source, game); + } + return false; + } + + @Override + public String getText(Mode mode) { + if ((staticText == null || staticText.isEmpty()) && this.effect != null) { // usefull for conditional night/day card abilities + return effect.getText(mode); + } + return staticText; + } + + @Override + public ConditionalPreventionEffect copy() { + return new ConditionalPreventionEffect(this); + } + +} diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java index 2970bbe3531..7cc9ed6c867 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java @@ -311,14 +311,6 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu } } return dependentToEffects; - /* - return allEffectsInLayer.stream() - .filter(effect -> effect.getDependencyTypes().contains(dependendToTypes)) - .map(Effect::getId) - .collect(Collectors.toSet()); - - } - return new HashSet<>();*/ } @Override diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index e6bb1a53da2..6cb9d0e7703 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -369,6 +369,7 @@ public class ContinuousEffects implements Serializable { replaceEffects.put(effect, applicableAbilities); } } + for (Iterator iterator = preventionEffects.iterator(); iterator.hasNext(); ) { PreventionEffect effect = iterator.next(); if (!effect.checksEventType(event, game)) { @@ -394,6 +395,7 @@ public class ContinuousEffects implements Serializable { replaceEffects.put(effect, applicableAbilities); } } + return replaceEffects; }