[SPM] Implement Lady Octopus, Inspired Inventor

This commit is contained in:
jmlundeen 2025-09-03 12:27:51 -05:00
parent 5bb8ff2c7f
commit e7636fb17d
5 changed files with 374 additions and 0 deletions

View file

@ -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);
}
}

View file

@ -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", 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'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("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("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", 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)); cards.add(new SetCardInfo("Mary Jane Watson", 229, Rarity.RARE, mage.cards.m.MaryJaneWatson.class, NON_FULL_USE_VARIOUS));

View file

@ -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);
}
}

View file

@ -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<UUID, List<UUID>> 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);
}
}

View file

@ -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<MageObject> {
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<MageObject> 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}";
}
}