[SPM] Implement The Soul Stone (#13936)

* [SPM] Implement The Soul Stone

* update to use permanent designation instead of game state

* update The Soul Stone according to release notes

* infinity ability is no longer on the card unless harnessed, which is only on the battlefield

* fix text on soul stone conditional ability

* update The Soul Stone

* create common effects for future Infinity cards
This commit is contained in:
Jmlundeen 2025-09-18 11:56:17 -05:00 committed by GitHub
parent ef7a511f0c
commit d886da6e52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 327 additions and 0 deletions

View file

@ -0,0 +1,67 @@
package mage.cards.t;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.common.ExileTargetCost;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.HarnessSourceEffect;
import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect;
import mage.abilities.effects.common.continuous.GainHarnessedAbilitySourceEffect;
import mage.abilities.keyword.IndestructibleAbility;
import mage.abilities.mana.BlackManaAbility;
import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.filter.StaticFilters;
import mage.target.common.TargetCardInYourGraveyard;
import mage.target.common.TargetControlledPermanent;
import java.util.UUID;
/**
*
* @author Jmlundeen
*/
public final class TheSoulStone extends CardImpl {
public TheSoulStone(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{B}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.INFINITY);
this.subtype.add(SubType.STONE);
// Indestructible
this.addAbility(IndestructibleAbility.getInstance());
// {T}: Add {B}.
this.addAbility(new BlackManaAbility());
// {6}{B}, {T}, Exile a creature you control: Harness The Soul Stone.
Ability ability = new SimpleActivatedAbility(new HarnessSourceEffect(), new ManaCostsImpl<>("{6}{B}"));
ability.addCost(new TapSourceCost());
ability.addCost(new ExileTargetCost(new TargetControlledPermanent(StaticFilters.FILTER_CONTROLLED_A_CREATURE)));
this.addAbility(ability);
// -- At the beginning of your upkeep, return target creature card from your graveyard to the battlefield.
Ability soulStoneAbility = new BeginningOfUpkeepTriggeredAbility(new ReturnFromGraveyardToBattlefieldTargetEffect());
soulStoneAbility.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD));
this.addAbility(new SimpleStaticAbility(
new GainHarnessedAbilitySourceEffect(soulStoneAbility))
);
}
private TheSoulStone(final TheSoulStone card) {
super(card);
}
@Override
public TheSoulStone copy() {
return new TheSoulStone(this);
}
}

View file

@ -275,6 +275,9 @@ public final class MarvelsSpiderMan extends ExpansionSet {
cards.add(new SetCardInfo("The Clone Saga", 28, Rarity.RARE, mage.cards.t.TheCloneSaga.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Death of Gwen Stacy", 223, Rarity.RARE, mage.cards.t.TheDeathOfGwenStacy.class, FULL_ART_USE_VARIOUS));
cards.add(new SetCardInfo("The Death of Gwen Stacy", 54, Rarity.RARE, mage.cards.t.TheDeathOfGwenStacy.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Soul Stone", 242, Rarity.MYTHIC, mage.cards.t.TheSoulStone.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Soul Stone", 243, Rarity.MYTHIC, mage.cards.t.TheSoulStone.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Soul Stone", 66, Rarity.MYTHIC, mage.cards.t.TheSoulStone.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Spot's Portal", 68, Rarity.UNCOMMON, mage.cards.t.TheSpotsPortal.class));
cards.add(new SetCardInfo("The Spot, Living Portal", 153, Rarity.RARE, mage.cards.t.TheSpotLivingPortal.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Spot, Living Portal", 231, Rarity.RARE, mage.cards.t.TheSpotLivingPortal.class, NON_FULL_USE_VARIOUS));

View file

@ -0,0 +1,121 @@
package org.mage.test.cards.single.spm;
import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility;
import mage.cards.Card;
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;
import static org.junit.Assert.assertTrue;
/**
*
* @author Jmlundeen
*/
public class TheSoulStoneTest extends CardTestPlayerBase {
/*
The Soul Stone
{1}{B}
Legendary Artifact - Infinity Stone
Indestructible
{T}: Add {B}.
{6}{B}, {T}, Exile a creature you control: Harness The Soul Stone.
-- At the beginning of your upkeep, return target creature card from your graveyard to the battlefield.
*/
private static final String theSoulStone = "The Soul Stone";
/*
Bear Cub
{1}{G}
Creature - Bear
2/2
*/
private static final String bearCub = "Bear Cub";
/*
Teferi's Time Twist
{1}{U}
Instant
Exile target permanent you control. Return that card to the battlefield under its owner's control at the beginning of the next end step. If it enters the battlefield as a creature, it enters with an additional +1/+1 counter on it.
*/
private static final String teferisTimeTwist = "Teferi's Time Twist";
@Test
public void testTheSoulStone() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, theSoulStone);
addCard(Zone.BATTLEFIELD, playerA, bearCub);
addCard(Zone.GRAVEYARD, playerA, bearCub);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 7);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{6}{B}, {T}");
setChoice(playerA, bearCub); // exile as cost
addTarget(playerA, bearCub); // return to battlefield
setStopAt(3, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, bearCub, 1);
assertExileCount(playerA, bearCub, 1);
// doesn't have ability if blinked
assertAbilityCount(playerA, theSoulStone, BeginningOfUpkeepTriggeredAbility.class, 1);
}
@Test
public void testBlinkSoulStone() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, theSoulStone);
addCard(Zone.BATTLEFIELD, playerA, bearCub);
addCard(Zone.GRAVEYARD, playerA, bearCub);
addCard(Zone.HAND, playerA, teferisTimeTwist);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 7);
addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{6}{B}, {T}");
setChoice(playerA, bearCub);
castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerA, teferisTimeTwist, theSoulStone);
setStopAt(3, PhaseStep.END_TURN);
execute();
assertExileCount(playerA, bearCub, 1);
assertPermanentCount(playerA, bearCub, 0);
assertGraveyardCount(playerA, bearCub, 1);
// doesn't have ability if blinked
assertAbilityCount(playerA, theSoulStone, BeginningOfUpkeepTriggeredAbility.class, 0);
}
@Test
public void testSoulStoneHasNoInfinityAbility() {
setStrictChooseMode(true);
removeAllCardsFromLibrary(playerA);
addCard(Zone.HAND, playerA, theSoulStone);
addCard(Zone.LIBRARY, playerA, theSoulStone);
addCard(Zone.GRAVEYARD, playerA, theSoulStone);
addCard(Zone.EXILED, playerA, theSoulStone);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
List<Card> cards = new ArrayList<>(getHandCards(playerA));
cards.addAll(getLibraryCards(playerA));
cards.addAll(getGraveCards(playerA));
cards.addAll(getExiledCards(playerA));
cards.forEach(card -> {
if (card.getName().equals(theSoulStone)) {
assertTrue("Should not have Infinity ability", !card.getAbilities(currentGame).containsClass(BeginningOfUpkeepTriggeredAbility.class));
}
});
}
}

View file

@ -0,0 +1,25 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author Jmlundee
*/
public enum SourceHarnessedCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanent(source.getSourceId());
return permanent != null && permanent.isHarnessed();
}
@Override
public String toString() {
return "{this} is harnessed";
}
}

View file

@ -0,0 +1,37 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author Jmlundeen
*/
public class HarnessSourceEffect extends OneShotEffect {
public HarnessSourceEffect() {
super(Outcome.AIDontUseIt);
staticText = "Harness {this}. <i>(Once harnessed, its ∞ ability is active.)<i>";
}
protected HarnessSourceEffect(final HarnessSourceEffect effect) {
super(effect);
}
@Override
public HarnessSourceEffect copy() {
return new HarnessSourceEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null) {
return false;
}
permanent.setHarnessed(true);
return true;
}
}

View file

@ -0,0 +1,58 @@
package mage.abilities.effects.common.continuous;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.Effect;
import mage.constants.Duration;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author Jmlundeen
*/
public class GainHarnessedAbilitySourceEffect extends ContinuousEffectImpl {
private final Ability ability;
public GainHarnessedAbilitySourceEffect(Effect effect) {
this(new SimpleStaticAbility(effect));
}
public GainHarnessedAbilitySourceEffect(Ability ability) {
super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
staticText = ability.getRule();
this.ability = ability;
this.ability.setRuleVisible(false);
generateGainAbilityDependencies(ability, null);
}
private GainHarnessedAbilitySourceEffect(final GainHarnessedAbilitySourceEffect effect) {
super(effect);
this.ability = effect.ability;
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null || !permanent.isHarnessed()) {
return false;
}
permanent.addAbility(ability, source.getSourceId(), game);
return true;
}
@Override
public GainHarnessedAbilitySourceEffect copy() {
return new GainHarnessedAbilitySourceEffect(this);
}
@Override
public String getText(Mode mode) {
return "∞ &mdash; " + super.getText(mode);
}
}

View file

@ -470,6 +470,10 @@ public interface Permanent extends Card, Controllable {
boolean solve(Game game, Ability source);
boolean isHarnessed();
void setHarnessed(boolean value);
@Override
Permanent copy();

View file

@ -72,6 +72,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
protected boolean monstrous;
protected boolean renowned;
protected boolean suspected;
protected boolean harnessed = false;
protected boolean manifested = false;
protected boolean cloaked = false;
protected boolean morphed = false;
@ -176,6 +177,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.monstrous = permanent.monstrous;
this.renowned = permanent.renowned;
this.suspected = permanent.suspected;
this.harnessed = permanent.harnessed;
this.ringBearerFlag = permanent.ringBearerFlag;
this.classLevel = permanent.classLevel;
this.goadingPlayers.addAll(permanent.goadingPlayers);
@ -2004,6 +2006,16 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
return true;
}
@Override
public boolean isHarnessed() {
return this.harnessed;
}
@Override
public void setHarnessed(boolean value) {
this.harnessed = value;
}
@Override
public boolean fight(Permanent fightTarget, Ability source, Game game) {
return this.fight(fightTarget, source, game, true);