diff --git a/Mage.Sets/src/mage/cards/a/AlaniaDivergentStorm.java b/Mage.Sets/src/mage/cards/a/AlaniaDivergentStorm.java new file mode 100644 index 00000000000..9edcef7913c --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AlaniaDivergentStorm.java @@ -0,0 +1,204 @@ +package mage.cards.a; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import mage.MageInt; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.Cost; +import mage.abilities.costs.CostImpl; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.CopyTargetStackObjectEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.stack.Spell; +import mage.players.Player; +import mage.target.common.TargetOpponent; +import mage.watchers.Watcher; + +/** + * + * @author jimga150 + */ +public final class AlaniaDivergentStorm extends CardImpl { + + public AlaniaDivergentStorm(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.OTTER); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(3); + this.toughness = new MageInt(5); + + // Whenever you cast a spell, if it's the first instant spell, the first sorcery spell, or the first Otter + // spell other than Alania you've cast this turn, you may have target opponent draw a card. If you do, copy + // that spell. You may choose new targets for the copy. + Ability ability = new ConditionalInterveningIfTriggeredAbility( + new SpellCastControllerTriggeredAbility(new DoIfCostPaid( + new CopyTargetStackObjectEffect(true), + new AlaniaDivergentStormCost() + ), null, false, SetTargetPointer.SPELL) + .setTriggerPhrase("Whenever you cast a spell, if it's the first instant spell, the first sorcery " + + "spell, or the first Otter spell other than Alania you've cast this turn, "), + AlaniaDivergentStormCondition.instance, "" + ); + ability.addWatcher(new AlaniaDivergentStormWatcher()); + this.addAbility(ability); + + } + + private AlaniaDivergentStorm(final AlaniaDivergentStorm card) { + super(card); + } + + @Override + public AlaniaDivergentStorm copy() { + return new AlaniaDivergentStorm(this); + } +} + +// Based on MarathWillOfTheWildRemoveCountersCost +class AlaniaDivergentStormCost extends CostImpl { + + AlaniaDivergentStormCost() { + this.text = "have target opponent draw a card"; + this.addTarget(new TargetOpponent()); + } + + private AlaniaDivergentStormCost(AlaniaDivergentStormCost cost) { + super(cost); + } + + @Override + public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { + Player player = game.getPlayer(controllerId); + if (player == null) { + return false; + } + for (UUID opponentID : game.getOpponents(controllerId)){ + Player opponent = game.getPlayer(opponentID); + if (opponent == null) { + continue; + } + if (opponent.canBeTargetedBy(source.getSourceObject(game), controllerId, source, game)) { + return true; + } + } + return false; + } + + @Override + public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { + this.getTargets().clearChosen(); + paid = false; + if (this.getTargets().choose(Outcome.DrawCard, controllerId, source.getSourceId(), source, game)) { + Player opponent = game.getPlayer(this.getTargets().getFirstTarget()); + if (opponent == null || !opponent.canRespond()){ + return false; + } + paid = opponent.drawCards(1, source, game) > 0; + } + return paid; + } + + @Override + public AlaniaDivergentStormCost copy() { + return new AlaniaDivergentStormCost(this); + } +} + +// Based on MastermindPlumCondition +enum AlaniaDivergentStormCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + Spell spell = (Spell) source.getEffects().get(0).getValue("spellCast"); + if (spell == null) { + return false; + } + Permanent sourcePermanent = source.getSourcePermanentOrLKI(game); + SpellAbility sourceCastAbility = sourcePermanent.getSpellAbility(); + // Get source permanent MOR from when it was on the stack + // The UUID of a spell on the stack is NOT the same as the card that produced it--it uses the UUID of the SpellAbility from that card instead (synced to the ZCC of the source Card). + MageObjectReference sourceSpellMOR = new MageObjectReference(sourceCastAbility.getId(), sourcePermanent.getZoneChangeCounter(game) - 1, game); + AlaniaDivergentStormWatcher watcher = game.getState().getWatcher(AlaniaDivergentStormWatcher.class); + UUID spellControllerID = spell.getControllerId(); + MageObjectReference spellMOR = new MageObjectReference(spell, game); + return watcher.spellIsFirstISOCast(spellControllerID, spellMOR, sourceSpellMOR); + } +} + +// Based on FirstSpellCastThisTurnWatcher +class AlaniaDivergentStormWatcher extends Watcher { + + private final Map playerFirstInstantCast = new HashMap<>(); + private final Map playerFirstSorceryCast = new HashMap<>(); + private final Map playerFirstOtterCast = new HashMap<>(); + private final Map playerSecondOtterCast = new HashMap<>(); + + public AlaniaDivergentStormWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.SPELL_CAST) { + return; + } + Spell spell = (Spell) game.getObject(event.getTargetId()); + if (spell == null) { + return; + } + UUID spellControllerID = spell.getControllerId(); + MageObjectReference spellMOR = new MageObjectReference(spell, game); + if (spell.getCardType(game).contains(CardType.INSTANT)) { + playerFirstInstantCast.putIfAbsent(spellControllerID, spellMOR); + } + if (spell.getCardType(game).contains(CardType.SORCERY)) { + playerFirstSorceryCast.putIfAbsent(spellControllerID, spellMOR); + } + if (spell.getSubtype(game).contains(SubType.OTTER)){ + if (playerFirstOtterCast.containsKey(spellControllerID)) { + // We already cast an otter this turn, put it on the second otter list + playerSecondOtterCast.putIfAbsent(spellControllerID, spellMOR); + } + // Will only put if we didnt cast an otter this turn yet + playerFirstOtterCast.putIfAbsent(spellControllerID, spellMOR); + } + } + + @Override + public void reset() { + super.reset(); + playerFirstInstantCast.clear(); + playerFirstSorceryCast.clear(); + playerFirstOtterCast.clear(); + playerSecondOtterCast.clear(); + } + + public boolean spellIsFirstISOCast(UUID controllerID, MageObjectReference spell, MageObjectReference AlaniaMOR) { + + MageObjectReference firstOtterMOR = playerFirstOtterCast.get(controllerID); + + if (firstOtterMOR != null && firstOtterMOR.equals(AlaniaMOR)) { + // The first otter we cast was the triggering Alania! check if the second otter matches instead + firstOtterMOR = playerSecondOtterCast.get(controllerID); + } + + return spell.equals(playerFirstInstantCast.get(controllerID)) || + spell.equals(playerFirstSorceryCast.get(controllerID)) || + (spell.equals(firstOtterMOR)); + } + +} diff --git a/Mage.Sets/src/mage/sets/Bloomburrow.java b/Mage.Sets/src/mage/sets/Bloomburrow.java index 265dead90f4..cacc5b9c3c8 100644 --- a/Mage.Sets/src/mage/sets/Bloomburrow.java +++ b/Mage.Sets/src/mage/sets/Bloomburrow.java @@ -25,6 +25,7 @@ public final class Bloomburrow extends ExpansionSet { cards.add(new SetCardInfo("Agate Assault", 122, Rarity.COMMON, mage.cards.a.AgateAssault.class)); cards.add(new SetCardInfo("Agate-Blade Assassin", 82, Rarity.COMMON, mage.cards.a.AgateBladeAssassin.class)); cards.add(new SetCardInfo("Alania's Pathmaker", 123, Rarity.COMMON, mage.cards.a.AlaniasPathmaker.class)); + cards.add(new SetCardInfo("Alania, Divergent Storm", 204, Rarity.RARE, mage.cards.a.AlaniaDivergentStorm.class)); cards.add(new SetCardInfo("Artist's Talent", 124, Rarity.RARE, mage.cards.a.ArtistsTalent.class)); cards.add(new SetCardInfo("Azure Beastbinder", 41, Rarity.RARE, mage.cards.a.AzureBeastbinder.class)); cards.add(new SetCardInfo("Bakersbane Duo", 163, Rarity.COMMON, mage.cards.b.BakersbaneDuo.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/blb/AlaniaDivergentStormTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/blb/AlaniaDivergentStormTest.java new file mode 100644 index 00000000000..c59ae60995d --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/blb/AlaniaDivergentStormTest.java @@ -0,0 +1,241 @@ +package org.mage.test.cards.single.blb; + +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.HasteAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author jimga150 + */ +public class AlaniaDivergentStormTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.a.AlaniaDivergentStorm Alania, Divergent Storm} {3}{U}{R} + * Legendary Creature — Otter Wizard + * Whenever you cast a spell, if it's the first instant spell, the first sorcery spell, or the first Otter spell + * other than Alania you've cast this turn, you may have target opponent draw a card. If you do, copy that spell. + * You may choose new targets for the copy. + */ + private static final String alania = "Alania, Divergent Storm"; + + @Test + public void test_TwoOtters() { + // Test that the "first Otter spell other than Alania you've cast this turn" clause works + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + addCard(Zone.HAND, playerA, "Coruscation Mage"); + addCard(Zone.HAND, playerA, alania); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, alania, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Coruscation Mage", true); + setChoice(playerA, "No"); // Offspring? + setChoice(playerA, "Yes"); // Copy spell? + setChoice(playerA, "PlayerB"); // Who draws? + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + // Copied + assertPermanentCount(playerA, "Coruscation Mage", 2); + // Not copied + assertPermanentCount(playerA, alania, 1); + // Opponent drew a card + assertHandCount(playerB, 1); + } + + @Test + public void test_TwoOttersNextTurn() { + // Test that the "first Otter spell other than Alania you've cast this turn" clause works on the next turn + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + addCard(Zone.HAND, playerA, "Coruscation Mage"); + addCard(Zone.HAND, playerA, "Stormcatch Mentor"); + addCard(Zone.HAND, playerA, alania); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, alania, true); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Stormcatch Mentor", true); + setChoice(playerA, "Yes"); // Copy spell? + setChoice(playerA, "PlayerB"); // Who draws? + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Coruscation Mage", true); + setChoice(playerA, "No"); // Offspring? + + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + // Copied + assertPermanentCount(playerA, "Stormcatch Mentor", 2); + // Not copied + assertPermanentCount(playerA, alania, 1); + assertPermanentCount(playerA, "Coruscation Mage", 1); + // Opponent drew a card (plus the one for turn draw) + assertHandCount(playerB, 1 + 1); + } + + @Test + public void test_ThreeOttersAdventureInstant() { + // Test that the "first Otter spell other than Alania you've cast this turn" clause excludes the third otter cast on the same turn you cast Alania + // Also throws in an adventure otter, cast for creature and instant + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 10); + addCard(Zone.HAND, playerA, "Coruscation Mage"); + addCard(Zone.HAND, playerA, "Frolicking Familiar", 2); + addCard(Zone.HAND, playerA, alania); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, alania, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Frolicking Familiar", true); + setChoice(playerA, "Yes"); // Copy spell? + setChoice(playerA, "PlayerB"); // Who draws? + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Coruscation Mage", true); + setChoice(playerA, "No"); // Offspring? + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Blow Off Steam", true); + setChoice(playerA, "Whenever you cast an instant", 2); // Add Frolicking Familiar triggers first + setChoice(playerA, "Whenever you cast a noncreature"); // Add Coruscation Mage trigger + // Alania's trigger will add last + setChoice(playerA, "Yes"); // Copy spell? + setChoice(playerA, "PlayerB"); // Who draws? + addTarget(playerA, playerB); + setChoice(playerA, "No"); // Change target? + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + // Copied + assertPermanentCount(playerA, "Frolicking Familiar", 2); + // Got boost from single cast of Blow Off Steam (copy =/= cast) + assertPowerToughness(playerA, "Frolicking Familiar", 3, 3); + // Not copied + assertPermanentCount(playerA, "Coruscation Mage", 1); + // Not copied + assertPermanentCount(playerA, alania, 1); + // Blow Off Steam copied, pinged twice, plus the Coruscation Mage ping + assertLife(playerB, currentGame.getStartingLife() - 3); + // opponent drew 2 cards + assertHandCount(playerB, 2); + } + + @Test + public void test_TwoInstants() { + // Test that the "first instant you've cast this turn" clause works + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.HAND, playerA, "Acrobatic Leap"); + addCard(Zone.HAND, playerA, "Ancestral Recall"); + addCard(Zone.HAND, playerA, alania); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, alania, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Acrobatic Leap", true); + setChoice(playerA, "Yes"); // Copy spell? + setChoice(playerA, "PlayerB"); // Who draws? + addTarget(playerA, alania); // Target creature + setChoice(playerA, "No"); // Change target? + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ancestral Recall", true); + addTarget(playerA, playerA); // Target player + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + // Copied: Acrobatic Leap + assertPowerToughness(playerA, alania, 3 + 2, 5 + 2*3); + // Not copied: Ancestral Recall + assertHandCount(playerA, 3); + // Opponent drew a card + assertHandCount(playerB, 1); + } + + @Test + public void test_TwoSorceries() { + // Test that the "first sorcery you've cast this turn" clause works + // Also copies an adventure sorcery + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + addCard(Zone.HAND, playerA, "Faerie Guidemother"); // adventure card + addCard(Zone.HAND, playerA, "Maximize Velocity"); + addCard(Zone.HAND, playerA, alania); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, alania, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gift of the Fae", true); + setChoice(playerA, "Yes"); // Copy spell? + setChoice(playerA, "PlayerB"); // Who draws? + addTarget(playerA, alania); // Target creature + setChoice(playerA, "No"); // Change target? + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Maximize Velocity", true); + addTarget(playerA, alania); // Target creature + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + // Copied: Gift of the Fae, Not copied: Ancestral Recall + assertPowerToughness(playerA, alania, 3 + 2*2 + 1, 5 + 2 + 1); + assertAbility(playerA, alania, FlyingAbility.getInstance(), true); + assertAbility(playerA, alania, HasteAbility.getInstance(), true); + // Opponent drew a card + assertHandCount(playerB, 1); + } + + @Test + public void test_OtherCard() { + // Test that card cast that is first of its type but is not an instant, sorcery, or otter will not trigger Alania + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + addCard(Zone.HAND, playerA, "Memnite"); + addCard(Zone.HAND, playerA, "Arcane Signet"); + addCard(Zone.HAND, playerA, "Ajani's Welcome"); + addCard(Zone.BATTLEFIELD, playerA, alania); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Memnite", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Arcane Signet", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ajani's Welcome", true); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + // Nothing copied + assertPermanentCount(playerA, "Memnite", 1); + assertPermanentCount(playerA, "Arcane Signet", 1); + assertPermanentCount(playerA, "Ajani's Welcome", 1); + // Opponent did not draw a card + assertHandCount(playerB, 0); + } + + @Test + public void test_TwoOttersOpponentsHexproof() { + // Test that Alania cannot copy spells if all opponents have hexproof + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.BATTLEFIELD, playerB, "Plains", 1); + addCard(Zone.HAND, playerA, "Coruscation Mage"); + addCard(Zone.BATTLEFIELD, playerA, alania); + addCard(Zone.HAND, playerB, "Blossoming Calm"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Blossoming Calm"); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Coruscation Mage", true); + setChoice(playerA, "No"); // Offspring? + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + // Not Copied + assertPermanentCount(playerA, "Coruscation Mage", 1); + } + +}