diff --git a/Mage.Sets/src/mage/cards/t/TorchTheTower.java b/Mage.Sets/src/mage/cards/t/TorchTheTower.java new file mode 100644 index 00000000000..88d06ed7016 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TorchTheTower.java @@ -0,0 +1,54 @@ +package mage.cards.t; + +import mage.abilities.condition.common.BargainedCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.replacement.DealtDamageToCreatureBySourceDies; +import mage.abilities.effects.keyword.ScryEffect; +import mage.abilities.keyword.BargainAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.target.common.TargetCreatureOrPlaneswalker; +import mage.watchers.common.DamagedByWatcher; + +import java.util.UUID; + +/** + * + * @author Susucr + */ +public final class TorchTheTower extends CardImpl { + + public TorchTheTower(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}"); + + // Bargain (You may sacrifice an artifact, enchantment, or token as you cast this spell.) + this.addAbility(new BargainAbility()); + + // Torch the Tower deals 2 damage to target creature or planeswalker. If this spell was bargained, instead it deals 3 damage to that permanent and you scry 1. + this.getSpellAbility().addEffect(new ConditionalOneShotEffect( + new DamageTargetEffect(3), + new DamageTargetEffect(2), + BargainedCondition.instance, + "{this} deals 2 damage to target creature or planeswalker. " + + "If this spell was bargained, instead it deals 3 damage to that permanent and you scry 1" + ).addEffect(new ScryEffect(1))); + this.getSpellAbility().addTarget(new TargetCreatureOrPlaneswalker()); + + // If a permanent dealt damage by Torch the Tower would die this turn, exile it instead. + this.getSpellAbility().addEffect(new DealtDamageToCreatureBySourceDies(this, Duration.EndOfTurn) + .setText("if a permanent dealt damage by {this} would die this turn, exile it instead")); + this.getSpellAbility().addWatcher(new DamagedByWatcher(false)); + } + + private TorchTheTower(final TorchTheTower card) { + super(card); + } + + @Override + public TorchTheTower copy() { + return new TorchTheTower(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TroublemakerOuphe.java b/Mage.Sets/src/mage/cards/t/TroublemakerOuphe.java new file mode 100644 index 00000000000..1b29211f920 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TroublemakerOuphe.java @@ -0,0 +1,64 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.TriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.BargainedCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.keyword.BargainAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.filter.FilterPermanent; +import mage.filter.predicate.Predicates; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class TroublemakerOuphe extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent("artifact or enchantment an opponent controls"); + + static { + filter.add(TargetController.OPPONENT.getControllerPredicate()); + filter.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.ENCHANTMENT.getPredicate() + )); + } + + public TroublemakerOuphe(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}"); + + this.subtype.add(SubType.OUPHE); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Bargain (You may sacrifice an artifact, enchantment, or token as you cast this spell.) + this.addAbility(new BargainAbility()); + + // When Troublemaker Ouphe enters the battlefield, if it was bargained, exile target artifact or enchantment an opponent controls. + TriggeredAbility trigger = new EntersBattlefieldTriggeredAbility(new ExileTargetEffect()); + trigger.addTarget(new TargetPermanent(filter)); + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + trigger, + BargainedCondition.instance, + "When {this} enters the battlefield, if it was bargained, exile target artifact or enchantment an opponent controls." + )); + } + + private TroublemakerOuphe(final TroublemakerOuphe card) { + super(card); + } + + @Override + public TroublemakerOuphe copy() { + return new TroublemakerOuphe(this); + } +} diff --git a/Mage.Sets/src/mage/sets/WildsOfEldraine.java b/Mage.Sets/src/mage/sets/WildsOfEldraine.java index b5b1bf7d390..2e1fdb1fada 100644 --- a/Mage.Sets/src/mage/sets/WildsOfEldraine.java +++ b/Mage.Sets/src/mage/sets/WildsOfEldraine.java @@ -42,6 +42,9 @@ public final class WildsOfEldraine extends ExpansionSet { cards.add(new SetCardInfo("Syr Ginger, the Meal Ender", 252, Rarity.RARE, mage.cards.s.SyrGingerTheMealEnder.class)); cards.add(new SetCardInfo("Talion, the Kindly Lord", 215, Rarity.MYTHIC, mage.cards.t.TalionTheKindlyLord.class)); cards.add(new SetCardInfo("Tanglespan Lookout", 188, Rarity.UNCOMMON, mage.cards.t.TanglespanLookout.class)); + cards.add(new SetCardInfo("Torch the Tower", 153, Rarity.COMMON, mage.cards.t.TorchTheTower.class)); + cards.add(new SetCardInfo("Tough Cookie", 193, Rarity.UNCOMMON, mage.cards.t.ToughCookie.class)); + cards.add(new SetCardInfo("Troublemaker Ouphe", 194, Rarity.COMMON, mage.cards.t.TroublemakerOuphe.class)); cards.add(new SetCardInfo("The Goose Mother", 204, Rarity.RARE, mage.cards.t.TheGooseMother.class)); cards.add(new SetCardInfo("Tough Cookie", 193, Rarity.UNCOMMON, mage.cards.t.ToughCookie.class)); cards.add(new SetCardInfo("Troyan, Gutsy Explorer", 217, Rarity.UNCOMMON, mage.cards.t.TroyanGutsyExplorer.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BargainTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BargainTest.java new file mode 100644 index 00000000000..8380fef973f --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BargainTest.java @@ -0,0 +1,230 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class BargainTest extends CardTestPlayerBase { + + /** + * Troublemaker Ouphe + * {1}{G} + * Creature — Ouphe + *
+ * Bargain (You may sacrifice an artifact, enchantment, or token as you cast this spell.) + * When Troublemaker Ouphe enters the battlefield, if it was bargained, exile target artifact or enchantment an opponent controls. + */ + private static final String troublemakerOuphe = "Troublemaker Ouphe"; + + // Artifact to be targetted by Troublemaker's Ouphe trigger, and be bargain fodder. + private static final String glider = "Aesthir Glider"; + // To blink Ouphe. + private static final String cloudshift = "Cloudshift"; + + /** + * Torch the Tower + * {R} + * Instant + *
+ * Bargain (You may sacrifice an artifact, enchantment, or token as you cast this spell.) + * Torch the Tower deals 2 damage to target creature or planeswalker. If this spell was bargained, instead it deals 3 damage to that permanent and you scry 1. + * If a permanent dealt damage by Torch the Tower would die this turn, exile it instead. + */ + private static final String torchTheTower = "Torch the Tower"; + + // 3 damage to target -- to finish off creatures and check torch's exile. + private static final String lightningBolt = "Lightning Bolt"; + + // 4/4 Artifact + private static final String stoneGolem = "Stone Golem"; + + @Test + public void testBargainNotPaidOuphe() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, troublemakerOuphe); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, glider); // Could be bargain. + + addCard(Zone.BATTLEFIELD, playerB, glider); + + setStrictChooseMode(true); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, troublemakerOuphe); + setChoice(playerA, false); // Do not bargain. + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerB, glider, 1); + } + + @Test + public void testBargainNothingToBargain() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, troublemakerOuphe); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + + addCard(Zone.BATTLEFIELD, playerB, glider); + + setStrictChooseMode(true); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, troublemakerOuphe); + // Nothing to bargain. + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerB, glider, 1); + } + + @Test + public void testBargainPaidOupheNothingToTarget() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, troublemakerOuphe); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, glider); // Is an artifact, hence something you can sac to Bargain. + + setStrictChooseMode(true); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, troublemakerOuphe); + setChoice(playerA, true); // true to bargain + setChoice(playerA, glider); // choose to sac glider. + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, glider, 1); + } + + @Test + public void testBargainPaidOuphe() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, troublemakerOuphe); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, glider); // Is an artifact, hence something you can sac to Bargain. + + addCard(Zone.BATTLEFIELD, playerB, glider); + + setStrictChooseMode(true); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, troublemakerOuphe); + setChoice(playerA, true); // true to bargain + setChoice(playerA, glider); // choose to sac glider. + addTarget(playerA, glider); // exile opponent's glider. + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerB, glider, 0); + assertExileCount(playerB, glider, 1); + assertGraveyardCount(playerA, glider, 1); + } + + @Test + public void testBargainPaidOupheThenFlicker() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, troublemakerOuphe); + addCard(Zone.HAND, playerA, cloudshift); + addCard(Zone.BATTLEFIELD, playerA, "Savannah", 3); + addCard(Zone.BATTLEFIELD, playerA, glider); // Is an artifact, hence something you can sac to Bargain. + + addCard(Zone.BATTLEFIELD, playerB, glider); + addCard(Zone.BATTLEFIELD, playerB, stoneGolem); + + setStrictChooseMode(true); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, troublemakerOuphe); + setChoice(playerA, true); // true to bargain + setChoice(playerA, glider); // choose to sac glider. + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); // let Ouphe resolve, trigger on the stack. + addTarget(playerA, glider); // exile opponent's glider with etb. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, cloudshift, troublemakerOuphe); // blink Ouphe. + // No trigger. It is no longer bargaining. + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerB, glider, 0); + assertExileCount(playerB, glider, 1); + assertPermanentCount(playerB, stoneGolem, 1); + assertExileCount(playerB, stoneGolem, 0); + assertGraveyardCount(playerA, glider, 1); + } + + @Test + public void testBargainNotPaidTorch() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, torchTheTower, 1); + addCard(Zone.HAND, playerA, lightningBolt, 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.BATTLEFIELD, playerA, glider); // Could be bargain. + + addCard(Zone.BATTLEFIELD, playerB, stoneGolem); + + setStrictChooseMode(true); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, torchTheTower, stoneGolem); + setChoice(playerA, false); // Do not bargain. + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertDamageReceived(playerB, stoneGolem, 2); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, lightningBolt, stoneGolem); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, glider, 1); + assertPermanentCount(playerB, stoneGolem, 0); + assertExileCount(playerB, stoneGolem, 1); + } + + @Test + public void testBargainPaidTorch() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, torchTheTower, 1); + addCard(Zone.HAND, playerA, lightningBolt, 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.BATTLEFIELD, playerA, glider); // Could be bargain. + + addCard(Zone.BATTLEFIELD, playerB, stoneGolem); + + setStrictChooseMode(true); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, torchTheTower, stoneGolem); + setChoice(playerA, true); // Do bargain. + setChoice(playerA, glider); // Bargain the glider away. + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertDamageReceived(playerB, stoneGolem, 3); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, lightningBolt, stoneGolem); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, glider, 0); + assertPermanentCount(playerB, stoneGolem, 0); + assertExileCount(playerB, stoneGolem, 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/BargainedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/BargainedCondition.java new file mode 100644 index 00000000000..cda1156d377 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/BargainedCondition.java @@ -0,0 +1,38 @@ +package mage.abilities.condition.common; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.keyword.BargainAbility; +import mage.cards.Card; +import mage.game.Game; + +/** + * Checks if the spell was cast with the alternate Bargain cost + * + * @author Susucr + */ +public enum BargainedCondition implements Condition { + + instance; + + @Override + public boolean apply(Game game, Ability source) { + // TODO: replace by Tag Cost Tracking. + MageObject sourceObject = source.getSourceObject(game); + if (sourceObject instanceof Card) { + for (Ability ability : ((Card) sourceObject).getAbilities(game)) { + if (ability instanceof BargainAbility) { + return ((BargainAbility) ability).wasBargained(game, source); + } + } + } + return false; + } + + @Override + public String toString() { + return "{this} was Bargained"; + } + +} diff --git a/Mage/src/main/java/mage/abilities/hint/ConditionTrueHint.java b/Mage/src/main/java/mage/abilities/hint/ConditionTrueHint.java new file mode 100644 index 00000000000..66c114a8b72 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/hint/ConditionTrueHint.java @@ -0,0 +1,58 @@ +package mage.abilities.hint; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.game.Game; +import mage.util.CardUtil; + +import java.awt.*; + +/** + * Displays an hint only when the condition is true. + * + * @author Susucr + */ +public class ConditionTrueHint implements Hint { + + private Condition condition; + private String trueText; + private Color trueColor; + private Boolean useIcons; + + public ConditionTrueHint(Condition condition) { + this(condition, condition.toString()); + } + + public ConditionTrueHint(Condition condition, String textWithIcons) { + this(condition, textWithIcons, null, true); + } + + public ConditionTrueHint(Condition condition, String trueText, Color trueColor, Boolean useIcons) { + this.condition = condition; + this.trueText = CardUtil.getTextWithFirstCharUpperCase(trueText); + this.trueColor = trueColor; + this.useIcons = useIcons; + } + + protected ConditionTrueHint(final ConditionTrueHint hint) { + this.condition = hint.condition; + this.trueText = hint.trueText; + this.trueColor = hint.trueColor; + this.useIcons = hint.useIcons; + } + + @Override + public String getText(Game game, Ability ability) { + String icon; + if (condition.apply(game, ability)) { + icon = this.useIcons ? HintUtils.HINT_ICON_GOOD : null; + return HintUtils.prepareText(this.trueText, this.trueColor, icon); + } + return ""; + } + + @Override + public Hint copy() { + return new ConditionTrueHint(this); + } +} diff --git a/Mage/src/main/java/mage/abilities/hint/common/BargainCostWasPaidHint.java b/Mage/src/main/java/mage/abilities/hint/common/BargainCostWasPaidHint.java new file mode 100644 index 00000000000..462fa942dc3 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/hint/common/BargainCostWasPaidHint.java @@ -0,0 +1,27 @@ +package mage.abilities.hint.common; + +import mage.abilities.Ability; +import mage.abilities.condition.common.BargainedCondition; +import mage.abilities.hint.ConditionTrueHint; +import mage.abilities.hint.Hint; +import mage.game.Game; + +/** + * @author Susucr + */ +public enum BargainCostWasPaidHint implements Hint { + + instance; + private static final ConditionTrueHint hint = new ConditionTrueHint(BargainedCondition.instance, "bargained"); + + @Override + public String getText(Game game, Ability ability) { + return hint.getText(game, ability); + } + + @Override + public Hint copy() { + return instance; + } + +} diff --git a/Mage/src/main/java/mage/abilities/keyword/BargainAbility.java b/Mage/src/main/java/mage/abilities/keyword/BargainAbility.java new file mode 100644 index 00000000000..f2bc9074fc0 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/BargainAbility.java @@ -0,0 +1,149 @@ + +package mage.abilities.keyword; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.StaticAbility; +import mage.abilities.costs.*; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.hint.common.BargainCostWasPaidHint; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.permanent.TokenPredicate; +import mage.game.Game; +import mage.players.Player; +import mage.util.CardUtil; + +/** + * Written before ruling was clarified. Feel free to put the ruling once it gets there. + *
+ * Bargain is a keyword static ability that adds an optional additional cost. + *
+ * Bargain means "You may sacrifice an artifact, enchantment, or token as you cast this spell". + *
+ * If a spell bargain cost is paid, the spell or the permanent it becomes is bargained.
+ *
+ * @author Susucr
+ */
+public class BargainAbility extends StaticAbility implements OptionalAdditionalSourceCosts {
+
+ private static final FilterControlledPermanent bargainFilter = new FilterControlledPermanent("an artifact, enchantment, or token");
+ private static final String promptString = "Bargain? (To Bargain, sacrifice an artifact, enchantment, or token)";
+ private static final String keywordText = "Bargain";
+ private static final String reminderText = "You may sacrifice an artifact, enchantment, or token as you cast this spell.";
+ private final String rule;
+
+ private String activationKey; // TODO: replace by Tag Cost Tracking.
+
+ protected OptionalAdditionalCost additionalCost;
+
+ static {
+ bargainFilter.add(Predicates.or(
+ CardType.ARTIFACT.getPredicate(),
+ CardType.ENCHANTMENT.getPredicate(),
+ TokenPredicate.TRUE
+ ));
+ }
+
+ public BargainAbility() {
+ super(Zone.STACK, null);
+ this.additionalCost = new OptionalAdditionalCostImpl(keywordText, reminderText, new SacrificeTargetCost(bargainFilter));
+ this.additionalCost.setRepeatable(false);
+ this.rule = additionalCost.getName() + additionalCost.getReminderText();
+ this.setRuleAtTheTop(true);
+ this.addHint(BargainCostWasPaidHint.instance);
+ this.activationKey = null;
+ }
+
+ private BargainAbility(final BargainAbility ability) {
+ super(ability);
+ this.rule = ability.rule;
+ this.additionalCost = ability.additionalCost.copy();
+ this.activationKey = ability.activationKey;
+ }
+
+ @Override
+ public BargainAbility copy() {
+ return new BargainAbility(this);
+ }
+
+ public void resetBargain() {
+ if (additionalCost != null) {
+ additionalCost.reset();
+ }
+ this.activationKey = null;
+ }
+
+ @Override
+ public void addOptionalAdditionalCosts(Ability ability, Game game) {
+ if (!(ability instanceof SpellAbility)) {
+ return;
+ }
+
+ Player player = game.getPlayer(ability.getControllerId());
+ if (player == null) {
+ return;
+ }
+
+ this.resetBargain();
+ boolean canPay = additionalCost.canPay(ability, this, ability.getControllerId(), game);
+ if (!canPay || !player.chooseUse(Outcome.Sacrifice, promptString, ability, game)) {
+ return;
+ }
+
+ additionalCost.activate();
+ for (Cost cost : ((Costs