diff --git a/Mage.Sets/src/mage/cards/t/TheSoulStone.java b/Mage.Sets/src/mage/cards/t/TheSoulStone.java new file mode 100644 index 00000000000..252ea24aafa --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheSoulStone.java @@ -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); + } +} diff --git a/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java b/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java index 8d8db43064d..bf09298e1c0 100644 --- a/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java +++ b/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java @@ -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)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/spm/TheSoulStoneTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/spm/TheSoulStoneTest.java new file mode 100644 index 00000000000..c61b8fc1655 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/spm/TheSoulStoneTest.java @@ -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 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)); + } + }); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/condition/common/SourceHarnessedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/SourceHarnessedCondition.java new file mode 100644 index 00000000000..7d5de8a4545 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/SourceHarnessedCondition.java @@ -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"; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/HarnessSourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/HarnessSourceEffect.java new file mode 100644 index 00000000000..a47aa31f40b --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/HarnessSourceEffect.java @@ -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}. (Once harnessed, its ∞ ability is active.)"; + } + + 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; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainHarnessedAbilitySourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainHarnessedAbilitySourceEffect.java new file mode 100644 index 00000000000..157a05bdb5b --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainHarnessedAbilitySourceEffect.java @@ -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 "∞ — " + super.getText(mode); + } +} diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index 73075f8f663..3b88aff182b 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -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(); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index c4a812dc4cb..23422c76a64 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -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);