Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[don’t merge] [BLB] Heirloom Epic, support AlternateManaPaymentAbility for abilities #12844

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import mage.game.match.Match;
import mage.game.permanent.Permanent;
import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.game.tournament.Tournament;
import mage.players.Player;
import mage.players.PlayerImpl;
Expand Down Expand Up @@ -1733,10 +1734,10 @@ protected void playManaAbilities(UUID objectId, Ability abilityToCast, ManaCost
// Reason: when you use special mana ability then normal mana abilities will be restricted to pay. Users
// can't see lands as playable and must know the reason (if they click on land then they get that message)
if (abilityToCast.getAbilityType() == AbilityType.SPELL) {
Spell spell = game.getStack().getSpell(abilityToCast.getSourceId());
StackObject spellOrAbility = game.getStack().getStackObject(abilityToCast.getSourceId());
boolean haveManaAbilities = object.getAbilities().stream().anyMatch(ManaAbility.class::isInstance);
if (spell != null && !spell.isResolving() && haveManaAbilities) {
switch (spell.getCurrentActivatingManaAbilitiesStep()) {
if (spellOrAbility != null && haveManaAbilities && (!(spellOrAbility instanceof Spell) || ((Spell) spellOrAbility).isResolving())) {
switch (spellOrAbility.getCurrentActivatingManaAbilitiesStep()) {
// if you used special mana ability like convoke then normal mana abilities will be restricted to use, see Convoke for details
case BEFORE:
case NORMAL:
Expand Down
218 changes: 218 additions & 0 deletions Mage.Sets/src/mage/cards/h/HeirloomEpic.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package mage.cards.h;

import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.SpecialAction;
import mage.abilities.common.ActivateAsSorceryActivatedAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ActivationManaAbilityStep;
import mage.abilities.costs.mana.AlternateManaPaymentAbility;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.hint.ValueHint;
import mage.abilities.mana.ManaOptions;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.predicate.permanent.TappedPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.stack.StackObject;
import mage.players.ManaPool;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetControlledPermanent;

import java.util.UUID;

/**
* @author notgreat
*/
public final class HeirloomEpic extends CardImpl {

public HeirloomEpic(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}");

// {4}, {T}: Draw a card. For each mana in this ability's activation cost, you may tap an untapped creature you control rather than pay that mana. Activate only as a sorcery.
this.addAbility(new HeirloomEpicActivatedAbility());
}

private HeirloomEpic(final HeirloomEpic card) {
super(card);
}

@Override
public HeirloomEpic copy() {
return new HeirloomEpic(this);
}
}

class HeirloomEpicActivatedAbility extends ActivateAsSorceryActivatedAbility {
HeirloomEpicActivatedAbility() {
super(new DrawCardSourceControllerEffect(1), new GenericManaCost(4));
addCost(new TapSourceCost());
addSubAbility(new HeirloomEpicPaymentAbility());
}

protected HeirloomEpicActivatedAbility(final HeirloomEpicActivatedAbility ability) {
super(ability);
}

@Override
public HeirloomEpicActivatedAbility copy() {
return new HeirloomEpicActivatedAbility(this);
}

@Override
public String getRule() {
String superRule = super.getRule();
return superRule.replaceFirst("Activate only as a sorcery",
"For each mana in this ability's activation cost, you may tap an untapped creature you control rather than pay that mana. Activate only as a sorcery");
}
}

// Based on ImproviseAbility
class HeirloomEpicPaymentAbility extends SimpleStaticAbility implements AlternateManaPaymentAbility {

private static final FilterControlledCreaturePermanent filterUntapped = new FilterControlledCreaturePermanent();

static {
filterUntapped.add(TappedPredicate.UNTAPPED);
}

private static final DynamicValue untappedCount = new PermanentsOnBattlefieldCount(filterUntapped);

public HeirloomEpicPaymentAbility() {
super(Zone.ALL, null); // all AlternateManaPaymentAbility must use ALL zone to calculate playable abilities
this.setRuleAtTheTop(true);
this.addHint(new ValueHint("Untapped creatures you control", untappedCount));
}

protected HeirloomEpicPaymentAbility(final HeirloomEpicPaymentAbility ability) {
super(ability);
}

@Override
public HeirloomEpicPaymentAbility copy() {
return new HeirloomEpicPaymentAbility(this);
}

@Override
public String getRule() {
return "";
}

@Override
public ActivationManaAbilityStep useOnActivationManaAbilityStep() {
return ActivationManaAbilityStep.AFTER;
}

@Override
public void addSpecialAction(Ability source, Game game, ManaCost unpaid) {
Player controller = game.getPlayer(source.getControllerId());
int canPayCount = untappedCount.calculate(game, source, null);
if (controller != null && canPayCount > 0) {
if (appliesToAbility(source) && unpaid.getMana().getGeneric() > 0) {
SpecialAction specialAction = new HeirloomEpicSpecialAction(unpaid, this);
specialAction.setControllerId(source.getControllerId());
specialAction.setSourceId(source.getSourceId());
// create filter for possible creatures to tap
Target target = new TargetControlledPermanent(1, unpaid.getMana().getGeneric(), filterUntapped, true);
target.withTargetName("creature to tap to pay ability cost");
specialAction.addTarget(target);
if (specialAction.canActivate(source.getControllerId(), game).canActivate()) {
game.getState().getSpecialActions().add(specialAction);
}
}
}
}

@Override
public ManaOptions getManaOptions(Ability source, Game game, ManaCost unpaid) {
ManaOptions options = new ManaOptions();
Player controller = game.getPlayer(source.getControllerId());
int canPayCount = untappedCount.calculate(game, source, null);
if (controller != null && canPayCount > 0) {
options.addMana(Mana.GenericMana(Math.min(unpaid.getMana().getGeneric(), canPayCount)));
}
return options;
}

@Override
public boolean appliesToAbility(Ability ability) {
return ability instanceof HeirloomEpicActivatedAbility;
}
}

class HeirloomEpicSpecialAction extends SpecialAction {

public HeirloomEpicSpecialAction(ManaCost unpaid, AlternateManaPaymentAbility manaAbility) {
super(Zone.ALL, manaAbility);
this.abilityType = AbilityType.SPECIAL_MANA_PAYMENT;
setRuleVisible(false);
this.addEffect(new HeirloomEpicEffect(unpaid));
}

protected HeirloomEpicSpecialAction(final HeirloomEpicSpecialAction ability) {
super(ability);
}

@Override
public HeirloomEpicSpecialAction copy() {
return new HeirloomEpicSpecialAction(this);
}
}

class HeirloomEpicEffect extends OneShotEffect {

private final ManaCost unpaid;

public HeirloomEpicEffect(ManaCost unpaid) {
super(Outcome.Benefit);
this.unpaid = unpaid;
this.staticText = "For each mana in this ability’s activation cost, you may tap an untapped creature you control rather than pay that mana";
}

protected HeirloomEpicEffect(final HeirloomEpicEffect effect) {
super(effect);
this.unpaid = effect.unpaid;
}

@Override
public HeirloomEpicEffect copy() {
return new HeirloomEpicEffect(this);
}

@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
StackObject ability = game.getStack().getStackObject(source.getSourceId());
if (controller == null || ability == null) {
return false;
}
for (UUID creatureId : this.getTargetPointer().getTargets(game, source)) {
Permanent perm = game.getPermanent(creatureId);
if (perm == null) {
continue;
}
if (!perm.isTapped() && perm.tap(source, game)) {
ManaPool manaPool = controller.getManaPool();
manaPool.addMana(Mana.ColorlessMana(1), game, source);
manaPool.unlockManaType(ManaType.COLORLESS);
if (!game.isSimulation()) {
game.informPlayers(controller.getLogName() + " taps " + perm.getLogName() + " to pay {1}");
}

// can't use mana abilities after that (tap cost must be paid after mana abilities only)
ability.setCurrentActivatingManaAbilitiesStep(ActivationManaAbilityStep.AFTER);
}
}
return true;
}
}
1 change: 1 addition & 0 deletions Mage.Sets/src/mage/sets/Bloomburrow.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ private Bloomburrow() {
cards.add(new SetCardInfo("Heaped Harvest", 175, Rarity.COMMON, mage.cards.h.HeapedHarvest.class));
cards.add(new SetCardInfo("Heartfire Hero", 138, Rarity.UNCOMMON, mage.cards.h.HeartfireHero.class));
cards.add(new SetCardInfo("Hearthborn Battler", 139, Rarity.RARE, mage.cards.h.HearthbornBattler.class));
cards.add(new SetCardInfo("Heirloom Epic", 246, Rarity.UNCOMMON, mage.cards.h.HeirloomEpic.class));
cards.add(new SetCardInfo("Helga, Skittish Seer", 217, Rarity.MYTHIC, mage.cards.h.HelgaSkittishSeer.class));
cards.add(new SetCardInfo("Hidden Grotto", 254, Rarity.COMMON, mage.cards.h.HiddenGrotto.class));
cards.add(new SetCardInfo("High Stride", 176, Rarity.COMMON, mage.cards.h.HighStride.class));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.mage.test.cards.single.blb;

import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;

/**
* @author notgreat
* Based on ImproviseTest by JayDi85
*/

public class HeirloomEpicTest extends CardTestPlayerBaseWithAIHelps {

@Test
public void test_PlayHeirloomEpic_Manual() {
// {4} to activate (payable by tapping)
addCard(Zone.BATTLEFIELD, playerA, "Heirloom Epic", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.BATTLEFIELD, playerA, "Alpha Myr", 2);

// use special action to pay (need disabled auto-payment and prepared mana pool)
disableManaAutoPayment(playerA);
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 2);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}, {T}:");
setChoice(playerA, "Blue", 2); // pay 1-2
setChoice(playerA, "For each");
addTarget(playerA, "Alpha Myr"); // pay 3 as tap
setChoice(playerA, "For each");
addTarget(playerA, "Alpha Myr"); // pay 4 as tap

setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();

assertHandCount(playerA, 1);
assertTappedCount("Heirloom Epic", true, 1);
assertTappedCount("Island", true, 2);
assertTappedCount("Alpha Myr", true, 2);
}

@Test
public void test_PlayHeirloomEpic_AI_AutoPay() {
// {4} to activate (payable by tapping)
addCard(Zone.BATTLEFIELD, playerA, "Heirloom Epic", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.BATTLEFIELD, playerA, "Alpha Myr", 2);

// AI must use special actions to pay
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}, {T}:");

setStrictChooseMode(false); // AI must choose targets
setStopAt(1, PhaseStep.END_TURN);
execute();

assertHandCount(playerA, 1);
assertTappedCount("Heirloom Epic", true, 1);
assertTappedCount("Island", true, 2);
assertTappedCount("Alpha Myr", true, 2);
}

@Test
public void test_PlayHeirloomEpic_AI_FullPlay() {
// {1} to cast, {4} to activate (payable by tapping)
addCard(Zone.HAND, playerA, "Heirloom Epic", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
addCard(Zone.BATTLEFIELD, playerA, "Alpha Myr", 2);

aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); //Cast the Heirloom Epic
aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); //Activate the Heirloom Epic

setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();

assertHandCount(playerA, 1);
assertTappedCount("Heirloom Epic", true, 1);
assertTappedCount("Island", true, 3);
assertTappedCount("Alpha Myr", true, 2);
}
}
7 changes: 1 addition & 6 deletions Mage/src/main/java/mage/abilities/SpecialAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import mage.constants.AbilityType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.stack.Spell;
import mage.game.stack.StackObject;

import java.util.UUID;

Expand Down Expand Up @@ -54,10 +52,7 @@ public ActivationStatus canActivate(UUID playerId, Game game) {
// limit play mana abilities by steps
int currentStepOrder = 0;
if (!game.getStack().isEmpty()) {
StackObject stackObject = game.getStack().getFirst();
if (stackObject instanceof Spell) {
currentStepOrder = ((Spell) stackObject).getCurrentActivatingManaAbilitiesStep().getStepOrder();
}
currentStepOrder = game.getStack().getFirst().getCurrentActivatingManaAbilitiesStep().getStepOrder();
}
if (currentStepOrder > manaAbility.useOnActivationManaAbilityStep().getStepOrder()) {
return ActivationStatus.getFalse();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mage.abilities.costs.mana;

import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.mana.ManaOptions;
import mage.game.Game;

Expand Down Expand Up @@ -38,4 +39,11 @@ public interface AlternateManaPaymentAbility {
* @return
*/
ActivationManaAbilityStep useOnActivationManaAbilityStep();
}

/**
* @return true if this AlternateManaPaymentAbility should apply to the given ability (by default checks if it is a SpellAbility)
*/
default boolean appliesToAbility(Ability ability) {
return ability instanceof SpellAbility;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public boolean apply(Game game, Ability source) {
game.informPlayers("Improvise: " + controller.getLogName() + " taps " + perm.getLogName() + " to pay {1}");
}

// can't use mana abilities after that (improvise cost must be payed after mana abilities only)
// can't use mana abilities after that (improvise cost must be paid after mana abilities only)
spell.setCurrentActivatingManaAbilitiesStep(ActivationManaAbilityStep.AFTER);
}

Expand Down
Loading