[DMU] Implement Thran Portal (#9456)

* [NCC] Implement Thran Portal

* Address comments

* Fixed usage of {this}
This commit is contained in:
Alex Vasile 2022-09-04 14:50:44 -04:00 committed by GitHub
parent c16ead128b
commit 09b069ceb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 295 additions and 0 deletions

View file

@ -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<SubType, BasicManaAbility> abilityMap = new HashMap<SubType, BasicManaAbility>() {{
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;
}
}

View file

@ -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 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 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("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("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("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)); cards.add(new SetCardInfo("Tidepool Turtle", 69, Rarity.COMMON, mage.cards.t.TidepoolTurtle.class));

View file

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