diff --git a/Mage.Sets/src/mage/cards/t/ThranPortal.java b/Mage.Sets/src/mage/cards/t/ThranPortal.java new file mode 100644 index 0000000000..ad880dbc80 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/ThranPortal.java @@ -0,0 +1,167 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.InvertCondition; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.ChooseBasicLandTypeEffect; +import mage.abilities.effects.common.TapSourceEffect; +import mage.abilities.effects.common.continuous.AddChosenSubtypeEffect; +import mage.abilities.effects.common.cost.CostModificationEffectImpl; +import mage.abilities.effects.common.enterAttribute.EnterAttributeAddChosenSubtypeEffect; +import mage.abilities.mana.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.common.FilterLandPermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class ThranPortal extends CardImpl { + + public ThranPortal(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); + + addSubType(SubType.GATE); + + // Thran Portal enters the battlefield tapped unless you control two or fewer other lands. + Condition controls = new InvertCondition(new PermanentsOnTheBattlefieldCondition(new FilterLandPermanent(), ComparisonType.FEWER_THAN, 3)); + String abilityText = " tapped unless you control two or fewer other lands"; + this.addAbility(new EntersBattlefieldAbility(new ConditionalOneShotEffect(new TapSourceEffect(), controls, abilityText), abilityText)); + + // As Thran Portal enters the battlefield, choose a basic land type. + // Thran Portal is the chosen type in addition to its other types. + AsEntersBattlefieldAbility chooseLandTypeAbility = new AsEntersBattlefieldAbility(new ChooseBasicLandTypeEffect(Outcome.AddAbility)); + chooseLandTypeAbility.addEffect(new EnterAttributeAddChosenSubtypeEffect()); // While it enters + this.addAbility(chooseLandTypeAbility); + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new AddChosenSubtypeEffect())); // While on the battlefield + + // Mana abilities of Thran Portal cost an additional 1 life to activate. + // This also adds the mana ability + this.addAbility(new SimpleStaticAbility(new ThranPortalAdditionalCostEffect())); + this.addAbility(new SimpleStaticAbility(new ThranPortalManaAbilityContinousEffect())); + } + + private ThranPortal(final ThranPortal card) { + super(card); + } + + @Override + public ThranPortal copy() { + return new ThranPortal(this); + } +} + +class ThranPortalManaAbilityContinousEffect extends ContinuousEffectImpl { + + private static final Map abilityMap = new HashMap() {{ + put(SubType.PLAINS, new WhiteManaAbility()); + put(SubType.ISLAND, new BlueManaAbility()); + put(SubType.SWAMP, new BlackManaAbility()); + put(SubType.MOUNTAIN, new RedManaAbility()); + put(SubType.FOREST, new GreenManaAbility()); + }}; + + public ThranPortalManaAbilityContinousEffect() { + super(Duration.WhileOnBattlefield, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Neutral); + staticText = "mana abilities of {this} cost an additional 1 life to activate"; + } + + public ThranPortalManaAbilityContinousEffect(final ThranPortalManaAbilityContinousEffect effect) { + super(effect); + } + + @Override + public ThranPortalManaAbilityContinousEffect copy() { + return new ThranPortalManaAbilityContinousEffect(this); + } + + @Override + public void init(Ability source, Game game) { + super.init(source, game); + SubType choice = SubType.byDescription((String) game.getState().getValue(source.getSourceId().toString() + ChooseBasicLandTypeEffect.VALUE_KEY)); + switch (choice) { + case FOREST: + dependencyTypes.add(DependencyType.BecomeForest); + break; + case PLAINS: + dependencyTypes.add(DependencyType.BecomePlains); + break; + case MOUNTAIN: + dependencyTypes.add(DependencyType.BecomeMountain); + break; + case ISLAND: + dependencyTypes.add(DependencyType.BecomeIsland); + break; + case SWAMP: + dependencyTypes.add(DependencyType.BecomeSwamp); + break; + default: + throw new RuntimeException("Incorrect mana choice " + choice + "for Thran Portal"); + } + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent thranPortal = game.getPermanent(source.getSourceId()); + SubType choice = SubType.byDescription((String) game.getState().getValue(source.getSourceId().toString() + ChooseBasicLandTypeEffect.VALUE_KEY)); + if (thranPortal == null || choice == null) { + return false; + } + + if (!thranPortal.hasSubtype(choice, game)) { + thranPortal.addSubType(choice); + } + if (!thranPortal.hasAbility(abilityMap.get(choice), game)) { + thranPortal.addAbility(abilityMap.get(choice), source.getId(), game); + } + + return true; + } +} + +class ThranPortalAdditionalCostEffect extends CostModificationEffectImpl { + + ThranPortalAdditionalCostEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit, CostModificationType.INCREASE_COST); + this.staticText = "mana abilities of {this} cost an additional 1 life to activate"; + } + + private ThranPortalAdditionalCostEffect(final ThranPortalAdditionalCostEffect effect) { + super(effect); + } + + @Override + public ThranPortalAdditionalCostEffect copy() { + return new ThranPortalAdditionalCostEffect(this); + } + + @Override + public boolean apply(Game game, Ability source, Ability abilityToModify) { + abilityToModify.addCost(new PayLifeCost(1)); + return true; + } + + @Override + public boolean applies(Ability abilityToModify, Ability source, Game game) { + if (!abilityToModify.getSourceId().equals(source.getSourceId())) { + return false; + } + + return abilityToModify instanceof ManaAbility; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/DominariaUnited.java b/Mage.Sets/src/mage/sets/DominariaUnited.java index 1fb40b230b..f90a3e7606 100644 --- a/Mage.Sets/src/mage/sets/DominariaUnited.java +++ b/Mage.Sets/src/mage/sets/DominariaUnited.java @@ -231,6 +231,7 @@ public final class DominariaUnited extends ExpansionSet { cards.add(new SetCardInfo("The Raven Man", 103, Rarity.RARE, mage.cards.t.TheRavenMan.class)); cards.add(new SetCardInfo("The Weatherseed Treaty", 188, Rarity.UNCOMMON, mage.cards.t.TheWeatherseedTreaty.class)); cards.add(new SetCardInfo("The World Spell", 189, Rarity.MYTHIC, mage.cards.t.TheWorldSpell.class)); + cards.add(new SetCardInfo("Thran Portal", 259, Rarity.RARE, mage.cards.t.ThranPortal.class)); cards.add(new SetCardInfo("Threats Undetected", 185, Rarity.RARE, mage.cards.t.ThreatsUndetected.class)); cards.add(new SetCardInfo("Thrill of Possibility", 148, Rarity.COMMON, mage.cards.t.ThrillOfPossibility.class)); cards.add(new SetCardInfo("Tidepool Turtle", 69, Rarity.COMMON, mage.cards.t.TidepoolTurtle.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/ThranPortalTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/ThranPortalTest.java new file mode 100644 index 0000000000..bc91ba78f6 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/ThranPortalTest.java @@ -0,0 +1,127 @@ +package org.mage.test.cards.single.dmu; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import java.lang.annotation.Target; + +/** + * {@link mage.cards.t.ThranPortal Thran Portal} + * Land Gate + * Thran Portal enters the battlefield tapped unless you control two or fewer other lands. + * As Thran Portal enters the battlefield, choose a basic land type. + * Thran Portal is the chosen type in addition to its other types. + * Mana abilities of Thran Portal cost an additional 1 life to activate. + * + * @author Alex-Vasile + */ +public class ThranPortalTest extends CardTestPlayerBase { + + private static final String thranPortal = "Thran Portal"; + + /** + * Test that tapping it for mana deals damage. + * Also tests that it comes in untapped if you control 2 of fewer lands. + */ + @Test + public void dealsDamage() { + String lightningBolt = "Lightning Bolt"; + addCard(Zone.HAND, playerA, thranPortal); + addCard(Zone.HAND, playerA, lightningBolt); + + setStrictChooseMode(true); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, thranPortal); + setChoice(playerA, "Thran"); + setChoice(playerA, "Mountain"); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, lightningBolt); + addTarget(playerA, playerB); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20 - 1); // 1 life for tapping it + assertLife(playerB, 20 - 3); // 3 life from lightning bolt + } + + + /** + * Test that it gets properly seen as the land type that was chosen. + * Also tests that it comes in tapped when you control 3 or more lands. + */ + @Test + public void seenAsChoice() { + // Whenever a Mountain enters the battlefield under your control, if you control at least five other Mountains, + // you may have Valakut, the Molten Pinnacle deal 3 damage to any target. + String valakut = "Valakut, the Molten Pinnacle"; + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, valakut); + addCard(Zone.HAND, playerA, thranPortal); + + setStrictChooseMode(true); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, thranPortal); + setChoice(playerA, "Thran"); + setChoice(playerA, "Mountain"); + setChoice(playerA, "Yes"); + addTarget(playerA, playerB); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + assertTapped(thranPortal, true); + assertLife(playerB, 20 - 3); + } + + /** + * Test that the mana ability gained from Chromatic Lantern also costs 1 life. + */ + @Test + public void chromaticLanternCostInteraction() { + // Lands you control have “{T}: Add one mana of any color.” + addCard(Zone.BATTLEFIELD, playerA, "Chromatic Lantern"); + addCard(Zone.HAND, playerA, thranPortal); + addCard(Zone.HAND, playerA, "Academy Loremaster"); // {U}{U} + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, thranPortal); + setChoice(playerA, "Thran"); + setChoice(playerA, "Mountain"); // Set mountain so that it must use the ability given by chromatic lantern to get the {U} + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Academy Loremaster"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 19); // Should have lost life from tapping Thran Portal to use the ability chromatic lantern gave it + assertPermanentCount(playerA, "Academy Loremaster" , 1); + } + + + /** + * Tests that Manascape Refractor copies the Thran Portal's mana abilities, but not the additional 1 life cost. + */ + @Test + public void manascapeRefractorInteraction() { + addCard(Zone.HAND, playerA, thranPortal); + addCard(Zone.HAND, playerA, "Academy Loremaster"); // {U}{U} + // Manascape Refractor enters the battlefield tapped. + // Manascape Refractor has all activated abilities of all lands on the battlefield. + // You may spend mana as though it were mana of any color to pay the activation costs of Manascape Refractor’s abilities. + addCard(Zone.BATTLEFIELD, playerA, "Manascape Refractor"); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, thranPortal); + setChoice(playerA, "Thran"); + setChoice(playerA, "Island"); // Both Thran portal and Manascape will not tap for blue + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Academy Loremaster"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20 - 1); // Lost one life for Thran portal BUT NOT for Manascape Refractor + assertPermanentCount(playerA, "Academy Loremaster" , 1); + } + +}