diff --git a/Mage.Sets/src/mage/cards/l/LadyOctopusInspiredInventor.java b/Mage.Sets/src/mage/cards/l/LadyOctopusInspiredInventor.java new file mode 100644 index 00000000000..a3389f0457e --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LadyOctopusInspiredInventor.java @@ -0,0 +1,60 @@ +package mage.cards.l; + +import mage.MageInt; +import mage.abilities.common.DrawNthOrNthCardTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.effects.common.cost.CastFromHandForFreeEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.common.FilterArtifactCard; +import mage.filter.predicate.mageobject.ManaValueCompareToCountersSourceCountPredicate; + +import java.util.UUID; + +/** + * + * @author Jmlundeen + */ +public final class LadyOctopusInspiredInventor extends CardImpl { + + private static final FilterCard filter = new FilterArtifactCard("an artifact spell from your hand with mana value less than or " + + "equal to the number of ingenuity counters on {this}"); + + static { + filter.add(new ManaValueCompareToCountersSourceCountPredicate(CounterType.INGENUITY, ComparisonType.OR_LESS)); + } + + public LadyOctopusInspiredInventor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SCIENTIST); + this.subtype.add(SubType.VILLAIN); + this.power = new MageInt(0); + this.toughness = new MageInt(2); + + // Whenever you draw your first or second card each turn, put an ingenuity counter on Lady Octopus. + this.addAbility(new DrawNthOrNthCardTriggeredAbility(new AddCountersSourceEffect(CounterType.INGENUITY.createInstance()))); + + // {T}: You may cast an artifact spell from your hand with mana value less than or equal to the number of ingenuity counters on Lady Octopus without paying its mana cost. + this.addAbility(new SimpleActivatedAbility(new CastFromHandForFreeEffect(filter), new TapSourceCost())); + } + + private LadyOctopusInspiredInventor(final LadyOctopusInspiredInventor card) { + super(card); + } + + @Override + public LadyOctopusInspiredInventor copy() { + return new LadyOctopusInspiredInventor(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java b/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java index 9373c729405..00f0aa6f338 100644 --- a/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java +++ b/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java @@ -90,6 +90,8 @@ public final class MarvelsSpiderMan extends ExpansionSet { cards.add(new SetCardInfo("Kraven's Last Hunt", 105, Rarity.RARE, mage.cards.k.KravensLastHunt.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Kraven's Last Hunt", 226, Rarity.RARE, mage.cards.k.KravensLastHunt.class, FULL_ART_USE_VARIOUS)); cards.add(new SetCardInfo("Kraven, Proud Predator", 132, Rarity.UNCOMMON, mage.cards.k.KravenProudPredator.class)); + cards.add(new SetCardInfo("Lady Octopus, Inspired Inventor", 252, Rarity.RARE, mage.cards.l.LadyOctopusInspiredInventor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Lady Octopus, Inspired Inventor", 35, Rarity.RARE, mage.cards.l.LadyOctopusInspiredInventor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Lurking Lizards", 107, Rarity.COMMON, mage.cards.l.LurkingLizards.class)); cards.add(new SetCardInfo("Mary Jane Watson", 134, Rarity.RARE, mage.cards.m.MaryJaneWatson.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Mary Jane Watson", 229, Rarity.RARE, mage.cards.m.MaryJaneWatson.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/spm/LadyOctopusInspiredInventorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/spm/LadyOctopusInspiredInventorTest.java new file mode 100644 index 00000000000..77d38c3fb6b --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/spm/LadyOctopusInspiredInventorTest.java @@ -0,0 +1,122 @@ +package org.mage.test.cards.single.spm; + +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.UntapAllControllerEffect; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.common.FilterCreaturePermanent; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author Jmlundeen + */ +public class LadyOctopusInspiredInventorTest extends CardTestPlayerBase { + + /* + Lady Octopus, Inspired Inventor + {U} + Legendary Creature - Human Scientist Villain + Whenever you draw your first or second card each turn, put an ingenuity counter on Lady Octopus. + {T}: You may cast an artifact spell from your hand with mana value less than or equal to the number of ingenuity counters on Lady Octopus without paying its mana cost. + */ + private static final String ladyOctopusInspiredInventor = "Lady Octopus, Inspired Inventor"; + + /* + Aether Vial + {1} + Artifact + At the beginning of your upkeep, you may put a charge counter on Aether Vial. + {T}: You may put a creature card with converted mana cost equal to the number of charge counters on ther Vial from your hand onto the battlefield. + */ + private static final String aetherVial = "Aether Vial"; + + /* + Tormod's Crypt + {0} + Artifact + {tap}, Sacrifice Tormod's Crypt: Exile all cards from target player's graveyard. + */ + private static final String tormodsCrypt = "Tormod's Crypt"; + + /* + Howling Mine + {2} + Artifact + At the beginning of each player's draw step, if Howling Mine is untapped, that player draws an additional card. + */ + private static final String howlingMine = "Howling Mine"; + + @Test + public void testLadyOctopusInspiredInventor() { + setStrictChooseMode(true); + + addCustomCardWithAbility("untap all creatures", playerA, new SimpleActivatedAbility( + new UntapAllControllerEffect(new FilterCreaturePermanent()), + new ManaCostsImpl<>("") + )); + addCustomCardWithAbility("draw a card", playerA, new SimpleActivatedAbility( + new DrawCardSourceControllerEffect(1), + new ManaCostsImpl<>("") + )); + addCard(Zone.BATTLEFIELD, playerA, ladyOctopusInspiredInventor); + addCard(Zone.HAND, playerA, aetherVial); + addCard(Zone.HAND, playerA, tormodsCrypt); + addCard(Zone.HAND, playerA, howlingMine); + + activateDrawCardAndUntap(); // Tormod's crypt + activateDrawCardAndUntap(); // Aether Vial + activateDrawCardAndUntap(); // Howling Mine + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertCounterCount(playerA, ladyOctopusInspiredInventor, CounterType.INGENUITY, 2); + assertHandCount(playerA, 3); + } + + private void activateDrawCardAndUntap() { + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: You may cast"); + setChoice(playerA, true); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "draw a"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "untap all"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + } + + @Test + public void testLadyOctopusInspiredInventorChoose() { + setStrictChooseMode(true); + + addCustomCardWithAbility("draw a card", playerA, new SimpleActivatedAbility( + new DrawCardSourceControllerEffect(3), + new ManaCostsImpl<>("") + )); + addCard(Zone.BATTLEFIELD, playerA, ladyOctopusInspiredInventor); + addCard(Zone.HAND, playerA, aetherVial); + addCard(Zone.HAND, playerA, tormodsCrypt); + addCard(Zone.HAND, playerA, howlingMine); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "draw "); + setChoice(playerA, "Whenever you draw your first"); // trigger stack + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: You may cast"); + setChoice(playerA, tormodsCrypt); + setChoice(playerA, true); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertCounterCount(playerA, ladyOctopusInspiredInventor, CounterType.INGENUITY, 2); + assertHandCount(playerA, 3 + 2); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/common/DrawNthOrNthCardTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DrawNthOrNthCardTriggeredAbility.java new file mode 100644 index 00000000000..84f4f210934 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/DrawNthOrNthCardTriggeredAbility.java @@ -0,0 +1,151 @@ +package mage.abilities.common; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.dynamicvalue.common.CardsDrawnThisTurnDynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.constants.TargetController; +import mage.constants.WatcherScope; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.util.CardUtil; +import mage.watchers.Watcher; + +import java.util.*; + +/** + * @author TheElk801 + */ +public class DrawNthOrNthCardTriggeredAbility extends TriggeredAbilityImpl { + + private static final Hint hint = new ValueHint( + "Cards drawn this turn", CardsDrawnThisTurnDynamicValue.instance + ); + private final TargetController targetController; + private final int firstCardNumber; + private final int secondCardNumber; + + public DrawNthOrNthCardTriggeredAbility(Effect effect) { + this(effect, false); + } + + public DrawNthOrNthCardTriggeredAbility(Effect effect, boolean optional) { + this(effect, optional, 1); + } + + public DrawNthOrNthCardTriggeredAbility(Effect effect, boolean optional, int firstCardNumber) { + this(effect, optional, TargetController.YOU, firstCardNumber); + } + + public DrawNthOrNthCardTriggeredAbility(Effect effect, boolean optional, TargetController targetController, int firstCardNumber) { + this(Zone.BATTLEFIELD, effect, optional, targetController, firstCardNumber, 2); + } + + public DrawNthOrNthCardTriggeredAbility(Zone zone, Effect effect, boolean optional, TargetController targetController, int firstCardNumber, int secondCardNumber) { + super(zone, effect, optional); + this.targetController = targetController; + this.firstCardNumber = firstCardNumber; + this.secondCardNumber = secondCardNumber; + if (targetController == TargetController.YOU) { + this.addHint(hint); + } + setTriggerPhrase(generateTriggerPhrase()); + this.addWatcher(new DrawNthOrNthCardWatcher()); + } + + protected DrawNthOrNthCardTriggeredAbility(final DrawNthOrNthCardTriggeredAbility ability) { + super(ability); + this.targetController = ability.targetController; + this.firstCardNumber = ability.firstCardNumber; + this.secondCardNumber = ability.secondCardNumber; + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DREW_CARD; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + switch (targetController) { + case YOU: + if (!isControlledBy(event.getPlayerId())) { + return false; + } + break; + case ACTIVE: + if (!game.isActivePlayer(event.getPlayerId())) { + return false; + } + break; + case OPPONENT: + if (!game.getOpponents(getControllerId()).contains(event.getPlayerId())) { + return false; + } + break; + case ANY: + // Doesn't matter who + break; + default: + throw new IllegalArgumentException("TargetController " + targetController + " not supported"); + } + int drawnCards = DrawNthOrNthCardWatcher.checkEvent(event.getPlayerId(), event.getId(), game) + 1; + return drawnCards == firstCardNumber || drawnCards == secondCardNumber; + } + + public String generateTriggerPhrase() { + String numberText = CardUtil.numberToOrdinalText(firstCardNumber) + " or " + CardUtil.numberToOrdinalText(secondCardNumber); + switch (targetController) { + case YOU: + return "Whenever you draw your " + numberText + " card each turn, "; + case ACTIVE: + return "Whenever a player draws their " + numberText + " card during their turn, "; + case OPPONENT: + return "Whenever an opponent draws their " + numberText + " card each turn, "; + case ANY: + return "Whenever a player draws their " + numberText + " card each turn, "; + default: + throw new IllegalArgumentException("TargetController " + targetController + " not supported"); + } + } + + @Override + public DrawNthOrNthCardTriggeredAbility copy() { + return new DrawNthOrNthCardTriggeredAbility(this); + } +} + +class DrawNthOrNthCardWatcher extends Watcher { + + private final Map> playerDrawEventMap = new HashMap<>(); + + DrawNthOrNthCardWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() == GameEvent.EventType.DREW_CARD) { + playerDrawEventMap + .computeIfAbsent(event.getPlayerId(), x -> new ArrayList<>()) + .add(event.getId()); + } + } + + @Override + public void reset() { + super.reset(); + playerDrawEventMap.clear(); + } + + static int checkEvent(UUID playerId, UUID eventId, Game game) { + return game + .getState() + .getWatcher(DrawNthOrNthCardWatcher.class) + .playerDrawEventMap + .getOrDefault(playerId, Collections.emptyList()) + .indexOf(eventId); + } +} diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/ManaValueCompareToCountersSourceCountPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/ManaValueCompareToCountersSourceCountPredicate.java new file mode 100644 index 00000000000..721cb9bbf99 --- /dev/null +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/ManaValueCompareToCountersSourceCountPredicate.java @@ -0,0 +1,39 @@ +package mage.filter.predicate.mageobject; + +import mage.MageObject; +import mage.constants.ComparisonType; +import mage.counters.CounterType; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; + +import java.util.Optional; + +/** + * @author jmlundeen + */ +public class ManaValueCompareToCountersSourceCountPredicate implements ObjectSourcePlayerPredicate { + + private final CounterType counterType; + private final ComparisonType comparisonType; + + public ManaValueCompareToCountersSourceCountPredicate(CounterType counterType, ComparisonType comparisonType) { + this.counterType = counterType; + this.comparisonType = comparisonType; + } + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + int counterCount = Optional + .ofNullable(input.getSource().getSourcePermanentOrLKI(game)) + .map(permanent -> permanent.getCounters(game)) + .map(counters -> counters.getCount(counterType)) + .orElse(-1); // always false + return ComparisonType.compare(input.getObject().getManaValue(), comparisonType, counterCount); + } + + @Override + public String toString() { + return "mana value " + comparisonType.toString() + " to the number of " + counterType.getName() + " counters on {this}"; + } +}