diff --git a/Mage.Sets/src/mage/cards/m/MoltenDisaster.java b/Mage.Sets/src/mage/cards/m/MoltenDisaster.java index 87c2d23db38..d5ed2814f18 100644 --- a/Mage.Sets/src/mage/cards/m/MoltenDisaster.java +++ b/Mage.Sets/src/mage/cards/m/MoltenDisaster.java @@ -3,25 +3,19 @@ package mage.cards.m; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.KickedCondition; -import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.dynamicvalue.common.ManacostVariableValue; +import mage.abilities.effects.common.DamageEverythingEffect; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.KickerAbility; +import mage.abilities.keyword.SplitSecondAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; import mage.constants.Zone; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.Predicates; import mage.filter.predicate.mageobject.AbilityPredicate; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import mage.players.Player; -import java.util.Optional; import java.util.UUID; /** @@ -29,18 +23,29 @@ import java.util.UUID; */ public final class MoltenDisaster extends CardImpl { + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature without flying"); + + static { + filter.add(Predicates.not(new AbilityPredicate(FlyingAbility.class))); + } + + private static final String rule = "if this spell was kicked, it has split second. " + + "(As long as this spell is on the stack, players can't cast spells or activate abilities that aren't mana abilities.)"; + public MoltenDisaster(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{R}{R}"); - // If Molten Disaster was kicked, it has split second. - Ability ability = new SimpleStaticAbility(Zone.STACK, new MoltenDisasterSplitSecondEffect()); + Ability ability = new SimpleStaticAbility(Zone.STACK, SplitSecondAbility.getSplitSecondEffectWithCondition(KickedCondition.ONCE) + .setText(rule)); ability.setRuleAtTheTop(true); this.addAbility(ability); + // Kicker {R} this.addAbility(new KickerAbility("{R}")); + // Molten Disaster deals X damage to each creature without flying and each player. - this.getSpellAbility().addEffect(new MoltenDisasterEffect()); + this.getSpellAbility().addEffect(new DamageEverythingEffect(ManacostVariableValue.REGULAR, filter)); } private MoltenDisaster(final MoltenDisaster card) { @@ -52,84 +57,3 @@ public final class MoltenDisaster extends CardImpl { return new MoltenDisaster(this); } } - -class MoltenDisasterSplitSecondEffect extends ContinuousRuleModifyingEffectImpl { - - MoltenDisasterSplitSecondEffect() { - super(Duration.WhileOnStack, Outcome.Detriment); - staticText = "if this spell was kicked, it has split second. (As long as this spell is on the stack, players can't cast spells or activate abilities that aren't mana abilities.)"; - } - - private MoltenDisasterSplitSecondEffect(final MoltenDisasterSplitSecondEffect effect) { - super(effect); - } - - @Override - public String getInfoMessage(Ability source, GameEvent event, Game game) { - return "You can't cast spells or activate abilities that aren't mana abilities (Split second)."; - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.CAST_SPELL || event.getType() == GameEvent.EventType.ACTIVATE_ABILITY; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - if (event.getType() == GameEvent.EventType.CAST_SPELL) { - if (KickedCondition.ONCE.apply(game, source)) { - return true; - } - } - if (event.getType() == GameEvent.EventType.ACTIVATE_ABILITY) { - Optional ability = game.getAbility(event.getTargetId(), event.getSourceId()); - return ability.isPresent() && !ability.get().isManaActivatedAbility() - && KickedCondition.ONCE.apply(game, source); - } - return false; - } - - @Override - public MoltenDisasterSplitSecondEffect copy() { - return new MoltenDisasterSplitSecondEffect(this); - } -} - -class MoltenDisasterEffect extends OneShotEffect { - - private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(); - - static { - filter.add(Predicates.not(new AbilityPredicate(FlyingAbility.class))); - } - - public MoltenDisasterEffect() { - super(Outcome.Damage); - staticText = "{this} deals X damage to each creature without flying and each player"; - } - - private MoltenDisasterEffect(final MoltenDisasterEffect effect) { - super(effect); - } - - @Override - public MoltenDisasterEffect copy() { - return new MoltenDisasterEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - int amount = source.getManaCostsToPay().getX(); - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), game)) { - permanent.damage(amount, source.getSourceId(), source, game, false, true); - } - for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { - Player player = game.getPlayer(playerId); - if (player != null) { - player.damage(amount, source.getSourceId(), source, game); - } - } - return true; - } - -} diff --git a/Mage.Sets/src/mage/cards/s/ScourgeOfTheSkyclaves.java b/Mage.Sets/src/mage/cards/s/ScourgeOfTheSkyclaves.java index 3109a8fca19..f2af217e91f 100644 --- a/Mage.Sets/src/mage/cards/s/ScourgeOfTheSkyclaves.java +++ b/Mage.Sets/src/mage/cards/s/ScourgeOfTheSkyclaves.java @@ -3,7 +3,7 @@ package mage.cards.s; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.condition.Condition; +import mage.abilities.condition.common.KickedCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; @@ -37,7 +37,7 @@ public final class ScourgeOfTheSkyclaves extends CardImpl { // When you cast this spell, if it was kicked, each player loses half their life, rounded up. this.addAbility(new ConditionalInterveningIfTriggeredAbility( - new CastSourceTriggeredAbility(new ScourgeOfTheSkyclavesEffect()), ScourgeOfTheSkyclavesCondition.instance, + new CastSourceTriggeredAbility(new ScourgeOfTheSkyclavesEffect()), KickedCondition.ONCE, "When you cast this spell, if it was kicked, each player loses half their life, rounded up." )); @@ -58,15 +58,6 @@ public final class ScourgeOfTheSkyclaves extends CardImpl { } } -enum ScourgeOfTheSkyclavesCondition implements Condition { - instance; - - @Override - public boolean apply(Game game, Ability source) { - return KickerAbility.getSpellKickedCount(game, source.getSourceId()) > 0; - } -} - enum ScourgeOfTheSkyclavesValue implements DynamicValue { instance; diff --git a/Mage.Sets/src/mage/cards/s/SowingMycospawn.java b/Mage.Sets/src/mage/cards/s/SowingMycospawn.java index b35237e4011..dc43d599d5f 100644 --- a/Mage.Sets/src/mage/cards/s/SowingMycospawn.java +++ b/Mage.Sets/src/mage/cards/s/SowingMycospawn.java @@ -2,7 +2,7 @@ package mage.cards.s; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.condition.Condition; +import mage.abilities.condition.common.KickedCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.CastSourceTriggeredAbility; import mage.abilities.effects.common.ExileTargetEffect; @@ -14,7 +14,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.filter.StaticFilters; -import mage.game.Game; import mage.target.common.TargetCardInLibrary; import mage.target.common.TargetLandPermanent; @@ -47,7 +46,7 @@ public final class SowingMycospawn extends CardImpl { // When you cast this spell, if it was kicked, exile target land. Ability ability = new ConditionalInterveningIfTriggeredAbility( new CastSourceTriggeredAbility(new ExileTargetEffect()), - SowingMycospawnCondition.instance, "When you cast this spell, " + + KickedCondition.ONCE, "When you cast this spell, " + "if it was kicked, exile target land." ); ability.addTarget(new TargetLandPermanent()); @@ -63,12 +62,3 @@ public final class SowingMycospawn extends CardImpl { return new SowingMycospawn(this); } } - -enum SowingMycospawnCondition implements Condition { - instance; - - @Override - public boolean apply(Game game, Ability source) { - return KickerAbility.getSpellKickedCount(game, source.getSourceId()) > 0; - } -} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java index 062b522cd9c..d69e26c04db 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java @@ -13,7 +13,7 @@ import org.mage.test.serverside.base.CardTestPlayerBase; */ public class KickerTest extends CardTestPlayerBase { - /** + /* * 702.32. Kicker 702.32a Kicker is a static ability that functions while * the spell with kicker is on the stack. “Kicker [cost]” means “You may pay * an additional [cost] as you cast this spell.” Paying a spell's kicker @@ -722,4 +722,49 @@ public class KickerTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Brain in a Jar", 1); } + + @Test + public void test_ConditionOnStackNotKicked() { + String scourge = "Scourge of the Skyclaves"; // 1B Creature + /* Kicker {4}{B} + When you cast this spell, if it was kicked, each player loses half their life, rounded up. + Scourge of the Skyclaves’s power and toughness are each equal to 20 minus the highest life total among players. + */ + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + addCard(Zone.HAND, playerA, scourge); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, scourge); + setChoice(playerA, false); // no kicker + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerA, scourge, 1); + } + + @Test + public void test_ConditionOnStackKicked() { + String scourge = "Scourge of the Skyclaves"; // 1B Creature + /* Kicker {4}{B} + When you cast this spell, if it was kicked, each player loses half their life, rounded up. + Scourge of the Skyclaves’s power and toughness are each equal to 20 minus the highest life total among players. + */ + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 7); + addCard(Zone.HAND, playerA, scourge); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, scourge); + setChoice(playerA, true); // kicked + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 10); + assertLife(playerB, 10); + assertPowerToughness(playerA, scourge, 10, 10); + } + } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/SplitSecondTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/SplitSecondTest.java index 8e93a91864b..e526b45979b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/SplitSecondTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/SplitSecondTest.java @@ -59,4 +59,90 @@ public class SplitSecondTest extends CardTestPlayerBase { assertLife(playerB, 20 - 2 - 2); assertPermanentCount(playerA, "Raging Goblin", 1); } + + private static final String molten = "Molten Disaster"; + /* {X}{R}{R} Sorcery + Kicker {R} + If this spell was kicked, it has split second. + Molten Disaster deals X damage to each creature without flying and each player. + */ + private static final String shock = "Shock"; + private static final String crab = "Fortress Crab"; // 1/6 + private static final String gnomes = "Bottle Gnomes"; // Sacrifice Bottle Gnomes: You gain 3 life. + private static final String bear = "Runeclaw Bear"; // 2/2 + private static final String drake = "Seacoast Drake"; // 1/3 flying + + public void setupMoltenDisaster() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + addCard(Zone.HAND, playerA, molten); + addCard(Zone.HAND, playerB, shock); + addCard(Zone.BATTLEFIELD, playerA, crab); + addCard(Zone.BATTLEFIELD, playerA, gnomes); + addCard(Zone.BATTLEFIELD, playerB, bear); + addCard(Zone.BATTLEFIELD, playerB, drake); + } + + @Test + public void testMoltenDisasterUnkicked() { + setupMoltenDisaster(); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, molten); + setChoice(playerA, false); // no kicker + setChoice(playerA, "X=1"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, shock, crab); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacrifice"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20 - 1 + 3); + assertLife(playerB, 20 - 1); + assertDamageReceived(playerA, crab, 1 + 2); + assertGraveyardCount(playerA, gnomes, 1); + assertDamageReceived(playerB, bear, 1); + assertDamageReceived(playerB, drake, 0); + } + + @Test + public void testMoltenDisasterKickedNoSpell() { + setupMoltenDisaster(); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, molten); + setChoice(playerA, true); // no kicker + setChoice(playerA, "X=1"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, shock, crab); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + + try { + execute(); + throw new AssertionError("expected failure to cast Shock"); + } catch (AssertionError e) { + Assert.assertTrue(e.getMessage().contains("Can't find ability to activate command: Cast Shock$target=Fortress Crab")); + } + + + } + + @Test + public void testMoltenDisasterKickedNoAbility() { + setupMoltenDisaster(); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, molten); + setChoice(playerA, true); // no kicker + setChoice(playerA, "X=1"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacrifice"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + + try { + execute(); + throw new AssertionError("expected failure to activate sacrifice ability"); + } catch (AssertionError e) { + Assert.assertTrue(e.getMessage().contains("Can't find ability to activate command: Sacrifice")); + } + + } + } diff --git a/Mage/src/main/java/mage/abilities/condition/common/KickedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/KickedCondition.java index e0b40c02e34..740409f3a14 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/KickedCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/KickedCondition.java @@ -24,7 +24,8 @@ public enum KickedCondition implements Condition { @Override public boolean apply(Game game, Ability source) { - return KickerAbility.getKickedCounter(game, source) >= kickedCount; + return KickerAbility.getKickedCounter(game, source) >= kickedCount // for on battlefield + || KickerAbility.getSpellKickedCount(game, source.getSourceId()) >= kickedCount; // for on stack } @Override diff --git a/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java b/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java index d19f0da7da6..fb9577dc9df 100644 --- a/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java @@ -105,9 +105,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo private void addKickerCostAndSetup(OptionalAdditionalCost newCost) { this.kickerCosts.add(newCost); - this.kickerCosts.forEach(cost -> { - cost.setCostType(VariableCostType.ADDITIONAL); - }); + this.kickerCosts.forEach(cost -> cost.setCostType(VariableCostType.ADDITIONAL)); } private void resetKicker() { @@ -124,10 +122,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo * Return total kicker activations with the specified Cost (blank for all kickers/multikickers) * Checks the start of the tags, to work for that blank method, which requires direct access * - * @param game - * @param source * @param needKickerCost use cost.getText(true) - * @return */ public static int getKickedCounterStrict(Game game, Ability source, String needKickerCost) { Map costsTags = CardUtil.getSourceCostsTagsMap(game, source); @@ -148,10 +143,6 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo /** * Return total kicker activations (kicker + multikicker) - * - * @param game - * @param source - * @return */ public static int getKickedCounter(Game game, Ability source) { return getKickedCounterStrict(game, source, ""); @@ -159,10 +150,6 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo /** * If spell was kicked - * - * @param game - * @param source - * @return */ public boolean isKicked(Game game, Ability source) { return isKicked(game, source, ""); @@ -171,10 +158,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo /** * If spell was kicked by specific kicker cost * - * @param game - * @param source * @param needKickerCost use cost.getText(true) - * @return */ public boolean isKicked(Game game, Ability source, String needKickerCost) { return getKickedCounterStrict(game, source, needKickerCost) > 0; @@ -287,10 +271,6 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo /** * Find spell's kicked stats. Must be used on stack only, e.g. for SPELL_CAST events - * - * @param game - * @param spellId - * @return */ public static int getSpellKickedCount(Game game, UUID spellId) { Spell spell = game.getSpellOrLKIStack(spellId); diff --git a/Mage/src/main/java/mage/abilities/keyword/SplitSecondAbility.java b/Mage/src/main/java/mage/abilities/keyword/SplitSecondAbility.java index 900a6641b8b..899b652fd0d 100644 --- a/Mage/src/main/java/mage/abilities/keyword/SplitSecondAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/SplitSecondAbility.java @@ -2,6 +2,8 @@ package mage.abilities.keyword; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.decorator.ConditionalContinuousRuleModifyingEffect; import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; import mage.constants.Duration; import mage.constants.Outcome; @@ -37,9 +39,13 @@ public class SplitSecondAbility extends SimpleStaticAbility { public SplitSecondAbility copy() { return new SplitSecondAbility(this); } -} -// Molten Disaster has a copy of this effect in it's class, so in case this effect has to be changed check also there + // For abilities that need the effect conditionally. Must set text manually. + public static ConditionalContinuousRuleModifyingEffect getSplitSecondEffectWithCondition(Condition condition) { + return new ConditionalContinuousRuleModifyingEffect(new SplitSecondEffect(), condition); + } + +} class SplitSecondEffect extends ContinuousRuleModifyingEffectImpl {