diff --git a/Mage.Sets/src/mage/cards/a/AltarOfTheWretched.java b/Mage.Sets/src/mage/cards/a/AltarOfTheWretched.java index d17c4d340e1..0c7e34a60d2 100644 --- a/Mage.Sets/src/mage/cards/a/AltarOfTheWretched.java +++ b/Mage.Sets/src/mage/cards/a/AltarOfTheWretched.java @@ -117,7 +117,7 @@ enum WretchedBonemassDynamicValue implements DynamicValue { ExileZone exileZone = game .getExile() .getExileZone(CardUtil.getExileZoneId( - game, permanent.getId(), permanent.getZoneChangeCounter(game) - 2 + game, permanent.getMainCard().getId(), permanent.getZoneChangeCounter(game) - 1 )); if (exileZone == null) { return 0; @@ -167,7 +167,7 @@ class WretchedBonemassGainAbilityEffect extends ContinuousEffectImpl { ExileZone exileZone = game .getExile() .getExileZone(CardUtil.getExileZoneId( - game, wretchedBonemass.getId(), wretchedBonemass.getZoneChangeCounter(game) - 2 + game, wretchedBonemass.getMainCard().getId(), wretchedBonemass.getZoneChangeCounter(game) - 1 )); if (exileZone != null && !exileZone.isEmpty()) { diff --git a/Mage.Sets/src/mage/cards/e/EyeOfOjerTaq.java b/Mage.Sets/src/mage/cards/e/EyeOfOjerTaq.java index ccb404fca3e..a5934e9a2ff 100644 --- a/Mage.Sets/src/mage/cards/e/EyeOfOjerTaq.java +++ b/Mage.Sets/src/mage/cards/e/EyeOfOjerTaq.java @@ -2,14 +2,15 @@ package mage.cards.e; import mage.MageObject; import mage.abilities.Ability; -import mage.abilities.SpellAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.AlternativeCostSourceAbility; import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.EntersBattlefieldEffect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.TapSourceEffect; -import mage.abilities.effects.common.cost.CostModificationEffectImpl; import mage.abilities.keyword.CraftAbility; import mage.abilities.mana.AnyColorManaAbility; import mage.cards.Card; @@ -27,6 +28,7 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCardInGraveyardBattlefieldOrStack; import mage.util.CardUtil; +import mage.watchers.common.SpellsCastWatcher; import java.util.*; import java.util.stream.Collectors; @@ -149,7 +151,10 @@ class ChooseCardTypeEffect extends OneShotEffect { if (permanent == null) { return false; } - ExileZone exileZone = game.getState().getExile().getExileZone(CardUtil.getExileZoneId(game, source, game.getState().getZoneChangeCounter(mageObject.getId()) - 1)); + ExileZone exileZone = game.getState() + .getExile() + .getExileZone(CardUtil + .getExileZoneId(game, permanent.getMainCard().getId(), permanent.getMainCard().getZoneChangeCounter(game))); if (exileZone == null) { return false; } @@ -232,63 +237,74 @@ class ApexObservatoryEffect extends OneShotEffect { } } -class ApexObservatoryCastWithoutManaEffect extends CostModificationEffectImpl { +class ApexObservatoryCastWithoutManaEffect extends ContinuousEffectImpl { + class ApexObservatoryCondition implements Condition { + private final int spellCastCount; + + private ApexObservatoryCondition(int spellCastCount) { + this.spellCastCount = spellCastCount; + } + + @Override + public boolean apply(Game game, Ability source) { + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + if (watcher != null) { + return watcher.getSpellsCastThisTurn(playerId).size() == spellCastCount; + } + return false; + } + } + + private final FilterCard filter; private final String chosenCardType; private final UUID playerId; - private boolean used = false; + private int spellCastCount; + private AlternativeCostSourceAbility alternativeCostSourceAbility; ApexObservatoryCastWithoutManaEffect(String chosenCardType, UUID playerId) { - super(Duration.EndOfTurn, Outcome.Benefit, CostModificationType.SET_COST); + super(Duration.EndOfTurn, Layer.RulesEffects, SubLayer.NA, Outcome.PlayForFree); this.chosenCardType = chosenCardType; this.playerId = playerId; + this.filter = new FilterCard("spell of the chosen type"); + filter.add(CardType.fromString(chosenCardType).getPredicate()); staticText = "The next spell you cast this turn of the chosen type can be cast without paying its mana cost"; } + @Override + public void init(Ability source, Game game) { + super.init(source, game); + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + if (watcher != null) { + spellCastCount = watcher.getSpellsCastThisTurn(playerId).size(); + Condition condition = new ApexObservatoryCondition(spellCastCount); + alternativeCostSourceAbility = new AlternativeCostSourceAbility( + null, condition, null, filter, true + ); + } + } + private ApexObservatoryCastWithoutManaEffect(final ApexObservatoryCastWithoutManaEffect effect) { super(effect); this.chosenCardType = effect.chosenCardType; this.playerId = effect.playerId; this.used = effect.used; + this.spellCastCount = effect.spellCastCount; + this.filter = effect.filter; + this.alternativeCostSourceAbility = effect.alternativeCostSourceAbility; } @Override - public boolean apply(Game game, Ability source, Ability abilityToModify) { + public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(playerId); - if (controller != null) { - MageObject spell = abilityToModify.getSourceObject(game); - if (spell != null && !game.isSimulation()) { - String message = "Cast " + spell.getIdName() + " without paying its mana cost?"; - if (controller.chooseUse(Outcome.Benefit, message, source, game)) { - abilityToModify.getManaCostsToPay().clear(); - used = true; - } - } + if (controller == null) { + return false; } + alternativeCostSourceAbility.setSourceId(source.getSourceId()); + controller.getAlternativeSourceCosts().add(alternativeCostSourceAbility); return true; } - @Override - public boolean isInactive(Ability source, Game game) { - return used || super.isInactive(source, game); - } - - @Override - public boolean applies(Ability ability, Ability source, Game game) { - if (used) { - return false; - } - if (!ability.isControlledBy(playerId)) { - return false; - } - if (!(ability instanceof SpellAbility)) { - return false; - } - MageObject object = game.getObject(ability.getSourceId()); - return object != null && object.getCardType(game).stream() - .anyMatch(cardType -> cardType.toString().equals(chosenCardType)); - } - @Override public ApexObservatoryCastWithoutManaEffect copy() { return new ApexObservatoryCastWithoutManaEffect(this); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/lcc/AltarOfTheWretchedTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/lcc/AltarOfTheWretchedTest.java new file mode 100644 index 00000000000..b829ca93a16 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/lcc/AltarOfTheWretchedTest.java @@ -0,0 +1,72 @@ +package org.mage.test.cards.single.lcc; + +import mage.abilities.Ability; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author Jmlundeen + */ +public class AltarOfTheWretchedTest extends CardTestPlayerBase { + + /* + Altar of the Wretched + {2}{B} + Artifact + When Altar of the Wretched enters the battlefield, you may sacrifice a nontoken creature. If you do, draw X cards, then mill X cards, where X is that creature's power. + Craft with one or more creatures {2}{B}{B} + {2}{B}: Return Altar of the Wretched from your graveyard to your hand. + + Wretched Bonemass + Color Indicator: Black + Creature — Skeleton Horror + Wretched Bonemass’s power and toughness are each equal to the total power of the exiled cards used to craft it. + This creature has flying as long as an exiled card used to craft it has flying. The same is true for first strike, double strike, deathtouch, haste, hexproof, indestructible, lifelink, menace, protection, reach, trample, and vigilance. + 0/0 + */ + private static final String altarOfTheWretched = "Altar of the Wretched"; + private static final String wretchedBoneMass = "Wretched Bonemass"; + + /* + Angel of Invention + {3}{W}{W} + Creature - Angel + Flying, vigilance, lifelink + Fabricate 2 + Other creatures you control get +1/+1. + 2/1 + */ + private static final String angelOfInvention = "Angel of Invention"; + + + @Test + public void testAltarOfTheWretched() { + addCard(Zone.BATTLEFIELD, playerA, altarOfTheWretched); + addCard(Zone.BATTLEFIELD, playerA, angelOfInvention); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Craft with one or more"); + addTarget(playerA, angelOfInvention); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, wretchedBoneMass, 2, 2); + assertExileCount(playerA, angelOfInvention, 1); + List abilities = new ArrayList<>(); + abilities.add(FlyingAbility.getInstance()); + abilities.add(VigilanceAbility.getInstance()); + abilities.add(LifelinkAbility.getInstance()); + assertAbilities(playerA, wretchedBoneMass, abilities); + } +} \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/lcc/EyeOfOjerTaqTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/lcc/EyeOfOjerTaqTest.java new file mode 100644 index 00000000000..2053ff2367f --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/lcc/EyeOfOjerTaqTest.java @@ -0,0 +1,86 @@ +package org.mage.test.cards.single.lcc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author Jmlundeen + */ +public class EyeOfOjerTaqTest extends CardTestPlayerBase { + + /* + Eye of Ojer Taq + {3} + Artifact + {T}: Add one mana of any color. + Craft with two that share a card type {6} + + Apex Observatory + Artifact + Apex Observatory enters the battlefield tapped. As it enters, choose a card type shared among two exiled cards used to craft it. + {T}: The next spell you cast this turn of the chosen type can be cast without paying its mana cost. + */ + private static final String eyeOfOjerTaq = "Eye of Ojer Taq"; + private static final String apexObservatory = "Apex Observatory"; + + /* + Lightning Bolt + {R} + Instant + Lightning Bolt deals 3 damage to any target. + */ + private static final String lightningBolt = "Lightning Bolt"; + + /* + Ponder + {U} + Sorcery + Look at the top three cards of your library, then put them back in any order. You may shuffle your library. + Draw a card. + */ + private static final String ponder = "Ponder"; + + /* + Shock + {R} + Instant + Shock deals 2 damage to any target. + */ + private static final String shock = "Shock"; + + @Test + public void testEyeOfOjerTaq() { + addCard(Zone.BATTLEFIELD, playerA, eyeOfOjerTaq); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 6); + addCard(Zone.GRAVEYARD, playerA, lightningBolt); + addCard(Zone.GRAVEYARD, playerA, shock); + addCard(Zone.HAND, playerA, shock, 2); + addCard(Zone.HAND, playerA, ponder); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Craft with two that share a card type"); + addTarget(playerA, lightningBolt + "^" + shock); + setChoice(playerA, "Instant"); + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: The next spell"); + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN, playerA); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, shock, playerB); // should be able to cast for free + setChoice(playerA, "Cast without paying"); + checkPlayableAbility("Can't cast second shock", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Shock", false); + + checkPlayableAbility("Can't cast ponder", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Ponder", false); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 2); // took 2 damage from shock + assertExileCount(playerA, lightningBolt, 1); + assertExileCount(playerA, shock, 1); + assertGraveyardCount(playerA, shock, 1); + assertPermanentCount(playerA, apexObservatory, 1); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/keyword/CraftAbility.java b/Mage/src/main/java/mage/abilities/keyword/CraftAbility.java index 1a1900d55fe..ca871fbffed 100644 --- a/Mage/src/main/java/mage/abilities/keyword/CraftAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/CraftAbility.java @@ -9,7 +9,6 @@ import mage.abilities.costs.common.ExileSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; -import mage.cards.TransformingDoubleFacedCardHalf; import mage.constants.*; import mage.filter.FilterCard; import mage.filter.FilterPermanent; @@ -122,7 +121,8 @@ class CraftCost extends CostImpl { @Override public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { Player player = game.getPlayer(source.getControllerId()); - if (player == null) { + Card sourceCard = game.getCard(source.getSourceId()); + if (player == null || sourceCard == null) { paid = false; return paid; } @@ -143,7 +143,7 @@ class CraftCost extends CostImpl { .collect(Collectors.toSet()); player.moveCardsToExile( cards, source, game, true, - CardUtil.getExileZoneId(game, source), + CardUtil.getExileZoneId(game, sourceCard.getMainCard().getId(), sourceCard.getMainCard().getZoneChangeCounter(game)), CardUtil.getSourceName(game, source) ); paid = true;