From 2a6b053e1735c2b85a1f297ed6ff4b117fe158c8 Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Sat, 15 Jun 2024 11:41:37 -0500 Subject: [PATCH] Added Eye of the Ojer Taq. --- .../src/mage/cards/a/ApexObservatory.java | 211 ++++++++++++++++++ Mage.Sets/src/mage/cards/e/EyeOfOjerTaq.java | 102 +++++++++ .../sets/LostCavernsOfIxalanCommander.java | 2 + 3 files changed, 315 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/a/ApexObservatory.java create mode 100644 Mage.Sets/src/mage/cards/e/EyeOfOjerTaq.java diff --git a/Mage.Sets/src/mage/cards/a/ApexObservatory.java b/Mage.Sets/src/mage/cards/a/ApexObservatory.java new file mode 100644 index 00000000000..99c13583faa --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/ApexObservatory.java @@ -0,0 +1,211 @@ +package mage.cards.a; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.EntersBattlefieldTappedAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.AlternativeCostSourceAbility; +import mage.abilities.costs.DynamicCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.choices.Choice; +import mage.choices.ChoiceCardType; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Layer; +import mage.constants.Outcome; +import mage.constants.SubLayer; +import mage.filter.FilterCard; +import mage.game.ExileZone; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.util.CardUtil; + +/** + * + * @author jeffwadsworth + */ +public class ApexObservatory extends CardImpl { + + public ApexObservatory(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, null); + + this.nightCard = true; + + // Apex Observatory enters the battlefield tapped. + this.addAbility(new EntersBattlefieldTappedAbility()); + + // Apex Observatory enters the battlefield tapped. As it enters, choose a card type shared among two exiled cards used to craft it. + this.addAbility(new AsEntersBattlefieldAbility(new ChooseCardTypeEffect())); + + // The next spell you cast this turn of the chosen type can be cast without paying its mana cost. + this.addAbility(new SimpleActivatedAbility(new ApexObservatoryEffect(), new TapSourceCost())); + } + + private ApexObservatory(final ApexObservatory card) { + super(card); + } + + @Override + public ApexObservatory copy() { + return new ApexObservatory(this); + } +} + +class ChooseCardTypeEffect extends OneShotEffect { + + public ChooseCardTypeEffect() { + super(Outcome.Neutral); + staticText = "choose a card type shared among two exiled cards used to craft it."; + } + + protected ChooseCardTypeEffect(final ChooseCardTypeEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + MageObject mageObject = game.getPermanentEntering(source.getSourceId()); + List exiledCardsCardType = new ArrayList<>(); + if (mageObject == null) { + mageObject = game.getObject(source); + } + if (controller != null && mageObject != null) { + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null) { + return false; + } + ExileZone exileZone = (ExileZone) game.getExile().getExileZone(CardUtil.getExileZoneId(game, source, +1)); + if (exileZone == null) { + return false; + } + for (Card card : exileZone.getCards(game)) { + exiledCardsCardType.addAll(card.getCardType(game)); + } + Choice cardTypeChoice = new ChoiceCardType(); + cardTypeChoice.getChoices().clear(); + cardTypeChoice.getChoices().addAll(exiledCardsCardType.stream().map(CardType::toString).collect(Collectors.toList())); + // find only card types that each card shares; some cards have more than 1 card type + Map cardTypeCounts = new HashMap<>(); + for (String cardType : cardTypeChoice.getChoices()) { + cardTypeCounts.put(cardType, 0); + } + + for (Card c : exileZone.getCards(game)) { + for (CardType cardType : c.getCardType(game)) { + if (cardTypeCounts.containsKey(cardType.toString())) { + cardTypeCounts.put(cardType.toString(), cardTypeCounts.get(cardType.toString()) + 1); + } + } + } + + List sharedCardTypes = new ArrayList<>(); + int numExiledCards = exileZone.getCards(game).size(); + for (Map.Entry entry : cardTypeCounts.entrySet()) { + if (entry.getValue() == numExiledCards) { + sharedCardTypes.add(entry.getKey()); + } + } + + cardTypeChoice.getChoices().retainAll(sharedCardTypes); + if (controller.choose(Outcome.Benefit, cardTypeChoice, game)) { + if (!game.isSimulation()) { + game.informPlayers(mageObject.getName() + ": " + controller.getLogName() + " has chosen " + cardTypeChoice.getChoice()); + } + // non-unique identifier for now until global one is setup. source won't work for the conditional check + game.getState().setValue("ApexObservatory" + source.getControllerId().toString(), cardTypeChoice.getChoice()); + if (mageObject instanceof Permanent) { + ((Permanent) mageObject).addInfo("chosen type", CardUtil.addToolTipMarkTags("Chosen card type: " + cardTypeChoice.getChoice()), game); + } + return true; + } + } + return false; + } + + @Override + public ChooseCardTypeEffect copy() { + return new ChooseCardTypeEffect(this); + } +} + +class ApexObservatoryEffect extends ContinuousEffectImpl { + + private static final FilterCard filter = new FilterCard("a spell"); + private final AlternativeCostSourceAbility alternativeCastingCostAbility = new ApexObservatoryAlternativeCostAbility( + new SpellMatchesChosenTypeCondition(), null, filter, true, null + ); + + public ApexObservatoryEffect() { + super(Duration.EndOfTurn, Layer.RulesEffects, SubLayer.NA, Outcome.Neutral); + staticText = "The next spell you cast this turn of the chosen type can be cast without paying its mana cost."; + } + + private ApexObservatoryEffect(final ApexObservatoryEffect effect) { + super(effect); + } + + @Override + public ApexObservatoryEffect copy() { + return new ApexObservatoryEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + controller.getAlternativeSourceCosts().add(alternativeCastingCostAbility); + return true; + } +} + +class ApexObservatoryAlternativeCostAbility extends AlternativeCostSourceAbility { + + public ApexObservatoryAlternativeCostAbility(Condition condition, String rule, FilterCard filter, boolean onlyMana, DynamicCost dynamicCost) { + super(condition, rule, filter, onlyMana, dynamicCost); + } + + @Override + public boolean isAvailable(Ability source, Game game) { + return super.isAvailable(source, game) && !AlternativeCostSourceAbility.getActivatedStatus(game, source, getOriginalId(), false); + } + + @Override + public ApexObservatoryAlternativeCostAbility copy() { + return new ApexObservatoryAlternativeCostAbility(condition, rule, filter, onlyMana, dynamicCost); + } +} + +class SpellMatchesChosenTypeCondition implements Condition { + + @Override + public boolean apply(Game game, Ability source) { + // investigating a global unique identifier for situations like this; source doesn't refer to the main card + return checkSpell(game.getObject(source), game, "ApexObservatory" + source.getControllerId().toString()); + } + + public static boolean checkSpell(MageObject spell, Game game, String key) { + if (spell instanceof Card) { + Card card = (Card) spell; + String chosenType = (String) game.getState().getValue(key); + return chosenType != null && card.getCardType(game).toString().contains(chosenType); + } + return false; + } +} diff --git a/Mage.Sets/src/mage/cards/e/EyeOfOjerTaq.java b/Mage.Sets/src/mage/cards/e/EyeOfOjerTaq.java new file mode 100644 index 00000000000..775a3ded557 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EyeOfOjerTaq.java @@ -0,0 +1,102 @@ +package mage.cards.e; + +import java.util.UUID; +import mage.abilities.Ability; +import mage.abilities.keyword.CraftAbility; +import mage.abilities.mana.AnyColorManaAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.TargetController; +import mage.filter.FilterCard; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.Game; +import mage.target.common.TargetCardInGraveyardBattlefieldOrStack; + +/** + * + * @author jeffwadsworth + */ +public class EyeOfOjerTaq extends CardImpl { + + public EyeOfOjerTaq(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}"); + + this.secondSideCardClazz = mage.cards.a.ApexObservatory.class; + + // {T}: Add one mana of any color. + this.addAbility(new AnyColorManaAbility()); + + // Craft with two that share a card type {6} ({6}, Exile this artifact, Exile the two from among other permanents you control + // and/or cards from your graveyard: Return this card transformed under its owner’s control. Craft only as a sorcery.) + this.addAbility(new CraftAbility( + "{6}", "two that share a card type", new EyeOfOjerTaqTarget() + )); + } + + private EyeOfOjerTaq(final EyeOfOjerTaq card) { + super(card); + } + + @Override + public EyeOfOjerTaq copy() { + return new EyeOfOjerTaq(this); + } + +} + +class EyeOfOjerTaqTarget extends TargetCardInGraveyardBattlefieldOrStack { + + private static final FilterCard filterCard + = new FilterCard(); + private static final FilterControlledPermanent filterPermanent + = new FilterControlledPermanent(); + + static { + filterCard.add(TargetController.YOU.getOwnerPredicate()); + filterPermanent.add(AnotherPredicate.instance); + + } + + EyeOfOjerTaqTarget() { + super(2, 2, filterCard, filterPermanent); + } + + private EyeOfOjerTaqTarget(final EyeOfOjerTaqTarget target) { + super(target); + } + + @Override + public EyeOfOjerTaqTarget copy() { + return new EyeOfOjerTaqTarget(this); + } + + @Override + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { + return false; + } + Card cardObject = game.getCard(id); + return cardObject != null + && this.getTargets() + .stream() + .map(game::getCard) + .noneMatch(c -> sharesCardtype(cardObject, c, game)); + } + + public static boolean sharesCardtype(Card card1, Card card2, Game game) { + if (card1.getId().equals(card2.getId())) { + return false; + } + // this should be returned true, but the invert works. + // if you note the code logic issue, please speak up. + for (CardType type : card1.getCardType(game)) { + if (card2.getCardType(game).contains(type)) { + return false; + } + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/LostCavernsOfIxalanCommander.java b/Mage.Sets/src/mage/sets/LostCavernsOfIxalanCommander.java index bbf1b886157..c9400da5bf8 100644 --- a/Mage.Sets/src/mage/sets/LostCavernsOfIxalanCommander.java +++ b/Mage.Sets/src/mage/sets/LostCavernsOfIxalanCommander.java @@ -31,6 +31,7 @@ public final class LostCavernsOfIxalanCommander extends ExpansionSet { cards.add(new SetCardInfo("Amulet of Vigor", 103, Rarity.RARE, mage.cards.a.AmuletOfVigor.class)); cards.add(new SetCardInfo("Angrath's Marauders", 215, Rarity.RARE, mage.cards.a.AngrathsMarauders.class)); cards.add(new SetCardInfo("Apex Altisaur", 232, Rarity.RARE, mage.cards.a.ApexAltisaur.class)); + cards.add(new SetCardInfo("Apex Observatory", 15, Rarity.MYTHIC, mage.cards.a.ApexObservatory.class)); cards.add(new SetCardInfo("Arcane Signet", 104, Rarity.UNCOMMON, mage.cards.a.ArcaneSignet.class)); cards.add(new SetCardInfo("Arch of Orazca", 319, Rarity.RARE, mage.cards.a.ArchOfOrazca.class)); cards.add(new SetCardInfo("Archaeomancer's Map", 101, Rarity.RARE, mage.cards.a.ArchaeomancersMap.class)); @@ -119,6 +120,7 @@ public final class LostCavernsOfIxalanCommander extends ExpansionSet { cards.add(new SetCardInfo("Everflowing Chalice", 111, Rarity.UNCOMMON, mage.cards.e.EverflowingChalice.class)); cards.add(new SetCardInfo("Evolution Sage", 240, Rarity.UNCOMMON, mage.cards.e.EvolutionSage.class)); cards.add(new SetCardInfo("Evolving Wilds", 328, Rarity.COMMON, mage.cards.e.EvolvingWilds.class)); + cards.add(new SetCardInfo("Eye of Ojer Taq", 15, Rarity.MYTHIC, mage.cards.e.EyeOfOjerTaq.class)); cards.add(new SetCardInfo("Exotic Orchard", 329, Rarity.RARE, mage.cards.e.ExoticOrchard.class)); cards.add(new SetCardInfo("Expedition Map", 112, Rarity.UNCOMMON, mage.cards.e.ExpeditionMap.class)); cards.add(new SetCardInfo("Explore", 241, Rarity.COMMON, mage.cards.e.Explore.class));