diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 2254f89e5f6..72e2735a641 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -4,15 +4,11 @@ import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; -import java.util.Arrays; -import java.util.List; - /** * @author TheElk801 */ public final class TarkirDragonstorm extends ExpansionSet { - private static final List unfinished = Arrays.asList("Channeled Dragonfire", "Glacial Dragonhunt", "Mammoth Bellow", "Nature's Rhythm", "Roamer's Routine", "Songcrafter Mage", "Synchronized Charge", "Unending Whisper", "Ureni's Rebuff", "Wild Ride", "Winternight Stories"); private static final TarkirDragonstorm instance = new TarkirDragonstorm(); public static TarkirDragonstorm getInstance() { @@ -297,7 +293,5 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Yathan Tombguard", 100, Rarity.UNCOMMON, mage.cards.y.YathanTombguard.class)); cards.add(new SetCardInfo("Zurgo's Vanguard", 133, Rarity.UNCOMMON, mage.cards.z.ZurgosVanguard.class)); cards.add(new SetCardInfo("Zurgo, Thunder's Decree", 237, Rarity.RARE, mage.cards.z.ZurgoThundersDecree.class)); - - cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HarmonizeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HarmonizeTest.java new file mode 100644 index 00000000000..5508f1e612c --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HarmonizeTest.java @@ -0,0 +1,55 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class HarmonizeTest extends CardTestPlayerBase { + + private static final String bear = "Grizzly Bears"; + private static final String dragonfire = "Channeled Dragonfire"; + + @Test + public void testNoTap() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 7); + addCard(Zone.BATTLEFIELD, playerA, bear); + addCard(Zone.GRAVEYARD, playerA, dragonfire); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Harmonize", playerB); + setChoice(playerA, false); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTapped(bear, false); + assertLife(playerB, 20 - 2); + assertExileCount(playerA, dragonfire, 1); + assertTappedCount("Mountain", true, 7); + } + + @Test + public void testTap() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, bear); + addCard(Zone.GRAVEYARD, playerA, dragonfire); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Harmonize", playerB); + setChoice(playerA, true); + setChoice(playerA, bear); + setChoice(playerA, bear); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTapped(bear, true); + assertLife(playerB, 20 - 2); + assertExileCount(playerA, dragonfire, 1); + assertTappedCount("Mountain", true, 5); + } +} diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 95d13c2d59e..2fd281c59a8 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -495,6 +495,7 @@ public abstract class AbilityImpl implements Ability { // A player can't apply two alternative methods of casting or two alternative costs to a single spell. switch (((SpellAbility) this).getSpellAbilityCastMode()) { case FLASHBACK: + case HARMONIZE: case MADNESS: case TRANSFORMED: case DISTURB: diff --git a/Mage/src/main/java/mage/abilities/keyword/CastFromGraveyardAbility.java b/Mage/src/main/java/mage/abilities/keyword/CastFromGraveyardAbility.java new file mode 100644 index 00000000000..73de96cf5e7 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/CastFromGraveyardAbility.java @@ -0,0 +1,197 @@ +package mage.abilities.keyword; + +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.Costs; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.cards.Card; +import mage.cards.ModalDoubleFacedCard; +import mage.cards.SplitCard; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * Base class for Flashback and Harmonize and any future ability which works similarly + * + * @author TheElk801 + */ +public abstract class CastFromGraveyardAbility extends SpellAbility { + + protected String abilityName; + private SpellAbility spellAbilityToResolve; + + protected CastFromGraveyardAbility(Card card, Cost cost, SpellAbilityCastMode spellAbilityCastMode) { + super(null, "", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE, spellAbilityCastMode); + this.setAdditionalCostsRuleVisible(false); + this.name = spellAbilityCastMode + " " + cost.getText(); + this.addCost(cost); + this.timing = card.isSorcery() ? TimingRule.SORCERY : TimingRule.INSTANT; + } + + protected CastFromGraveyardAbility(final CastFromGraveyardAbility ability) { + super(ability); + this.abilityName = ability.abilityName; + this.spellAbilityToResolve = ability.spellAbilityToResolve; + } + + @Override + public ActivationStatus canActivate(UUID playerId, Game game) { + // flashback ability dynamicly added to all card's parts (split cards) + if (!super.canActivate(playerId, game).canActivate()) { + return ActivationStatus.getFalse(); + } + Card card = game.getCard(getSourceId()); + if (card == null) { + return ActivationStatus.getFalse(); + } + // Card must be in the graveyard zone + if (game.getState().getZone(card.getId()) != Zone.GRAVEYARD) { + return ActivationStatus.getFalse(); + } + // Cards with no Mana Costs cant't be flashbacked (e.g. Ancestral Vision) + if (card.getManaCost().isEmpty()) { + return ActivationStatus.getFalse(); + } + // CastFromGraveyard can never cast a split card by Fuse, because Fuse only works from hand + // https://tappedout.net/mtg-questions/snapcaster-mage-and-flashback-on-a-fuse-card-one-or-both-halves-legal-targets/ + if (card instanceof SplitCard) { + if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { + return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); + } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { + return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); + } + } else if (card instanceof ModalDoubleFacedCard) { + if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { + return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); + } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { + return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); + } + } + return card.getSpellAbility().canActivate(playerId, game); + } + + @Override + public SpellAbility getSpellAbilityToResolve(Game game) { + Card card = game.getCard(getSourceId()); + if (card == null || spellAbilityToResolve != null) { + return spellAbilityToResolve; + } + SpellAbility spellAbilityCopy; + if (card instanceof SplitCard) { + if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { + spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy(); + } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { + spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy(); + } else { + spellAbilityCopy = null; + } + } else if (card instanceof ModalDoubleFacedCard) { + if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { + spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy(); + } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { + spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy(); + } else { + spellAbilityCopy = null; + } + } else { + spellAbilityCopy = card.getSpellAbility().copy(); + } + if (spellAbilityCopy == null) { + return null; + } + spellAbilityCopy.setId(this.getId()); + spellAbilityCopy.clearManaCosts(); + spellAbilityCopy.clearManaCostsToPay(); + spellAbilityCopy.addCost(this.getCosts().copy()); + spellAbilityCopy.addCost(this.getManaCosts().copy()); + spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode()); + spellAbilityToResolve = spellAbilityCopy; + ContinuousEffect effect = new CastFromGraveyardReplacementEffect(); + effect.setTargetPointer(new FixedTarget(getSourceId(), game.getState().getZoneChangeCounter(getSourceId()))); + game.addEffect(effect, this); + return spellAbilityToResolve; + } + + @Override + public Costs getCosts() { + if (spellAbilityToResolve == null) { + return super.getCosts(); + } + return spellAbilityToResolve.getCosts(); + } + + @Override + public String getRule(boolean all) { + return this.getRule(); + } + + /** + * Used for split card in PlayerImpl method: + * getOtherUseableActivatedAbilities + * + * @param abilityName + */ + public CastFromGraveyardAbility setAbilityName(String abilityName) { + this.abilityName = abilityName; + return this; + } +} + +class CastFromGraveyardReplacementEffect extends ReplacementEffectImpl { + + public CastFromGraveyardReplacementEffect() { + super(Duration.OneUse, Outcome.Exile); + } + + protected CastFromGraveyardReplacementEffect(final CastFromGraveyardReplacementEffect effect) { + super(effect); + } + + @Override + public CastFromGraveyardReplacementEffect copy() { + return new CastFromGraveyardReplacementEffect(this); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + Card card = game.getCard(event.getTargetId()); + if (card == null) { + return false; + } + discard(); + return controller.moveCards( + card, Zone.EXILED, source, game, false, + false, false, event.getAppliedEffects() + ); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + UUID cardId = CardUtil.getMainCardId(game, source.getSourceId()); // for split cards + if (!cardId.equals(event.getTargetId()) + || ((ZoneChangeEvent) event).getFromZone() != Zone.STACK + || ((ZoneChangeEvent) event).getToZone() == Zone.EXILED) { + return false; + } + int zcc = game.getState().getZoneChangeCounter(cardId); + return ((FixedTarget) getTargetPointer()).getZoneChangeCounter() + 1 == zcc; + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java b/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java index e88cf333684..4b7dae1d709 100644 --- a/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java @@ -1,23 +1,8 @@ package mage.abilities.keyword; -import mage.abilities.Ability; -import mage.abilities.SpellAbility; import mage.abilities.costs.Cost; -import mage.abilities.costs.Costs; -import mage.abilities.effects.ContinuousEffect; -import mage.abilities.effects.ReplacementEffectImpl; import mage.cards.Card; -import mage.cards.ModalDoubleFacedCard; -import mage.cards.SplitCard; -import mage.constants.*; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.ZoneChangeEvent; -import mage.players.Player; -import mage.target.targetpointer.FixedTarget; -import mage.util.CardUtil; - -import java.util.UUID; +import mage.constants.SpellAbilityCastMode; /** * 702.32. Flashback @@ -33,106 +18,14 @@ import java.util.UUID; * * @author nantuko */ -public class FlashbackAbility extends SpellAbility { - - private String abilityName; - private SpellAbility spellAbilityToResolve; +public class FlashbackAbility extends CastFromGraveyardAbility { public FlashbackAbility(Card card, Cost cost) { - super(null, "", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.FLASHBACK); - this.setAdditionalCostsRuleVisible(false); - this.name = "Flashback " + cost.getText(); - this.addCost(cost); - this.timing = card.isSorcery() ? TimingRule.SORCERY : TimingRule.INSTANT; + super(card, cost, SpellAbilityCastMode.FLASHBACK); } protected FlashbackAbility(final FlashbackAbility ability) { super(ability); - this.spellAbilityType = ability.spellAbilityType; - this.abilityName = ability.abilityName; - this.spellAbilityToResolve = ability.spellAbilityToResolve; - } - - @Override - public ActivationStatus canActivate(UUID playerId, Game game) { - // flashback ability dynamicly added to all card's parts (split cards) - if (super.canActivate(playerId, game).canActivate()) { - Card card = game.getCard(getSourceId()); - if (card != null) { - // Card must be in the graveyard zone - if (game.getState().getZone(card.getId()) != Zone.GRAVEYARD) { - return ActivationStatus.getFalse(); - } - // Cards with no Mana Costs cant't be flashbacked (e.g. Ancestral Vision) - if (card.getManaCost().isEmpty()) { - return ActivationStatus.getFalse(); - } - // Flashback can never cast a split card by Fuse, because Fuse only works from hand - // https://tappedout.net/mtg-questions/snapcaster-mage-and-flashback-on-a-fuse-card-one-or-both-halves-legal-targets/ - if (card instanceof SplitCard) { - if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { - return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); - } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { - return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); - } - } else if (card instanceof ModalDoubleFacedCard) { - if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { - return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); - } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { - return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); - } - } - return card.getSpellAbility().canActivate(playerId, game); - } - } - return ActivationStatus.getFalse(); - } - - @Override - public SpellAbility getSpellAbilityToResolve(Game game) { - Card card = game.getCard(getSourceId()); - if (card != null) { - if (spellAbilityToResolve == null) { - SpellAbility spellAbilityCopy = null; - if (card instanceof SplitCard) { - if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { - spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy(); - } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { - spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy(); - } - } else if (card instanceof ModalDoubleFacedCard) { - if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { - spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy(); - } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { - spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy(); - } - } else { - spellAbilityCopy = card.getSpellAbility().copy(); - } - if (spellAbilityCopy == null) { - return null; - } - spellAbilityCopy.setId(this.getId()); - spellAbilityCopy.clearManaCosts(); - spellAbilityCopy.clearManaCostsToPay(); - spellAbilityCopy.addCost(this.getCosts().copy()); - spellAbilityCopy.addCost(this.getManaCosts().copy()); - spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode()); - spellAbilityToResolve = spellAbilityCopy; - ContinuousEffect effect = new FlashbackReplacementEffect(); - effect.setTargetPointer(new FixedTarget(getSourceId(), game.getState().getZoneChangeCounter(getSourceId()))); - game.addEffect(effect, this); - } - } - return spellAbilityToResolve; - } - - @Override - public Costs getCosts() { - if (spellAbilityToResolve == null) { - return super.getCosts(); - } - return spellAbilityToResolve.getCosts(); } @Override @@ -140,11 +33,6 @@ public class FlashbackAbility extends SpellAbility { return new FlashbackAbility(this); } - @Override - public String getRule(boolean all) { - return this.getRule(); - } - @Override public String getRule() { StringBuilder sbRule = new StringBuilder("Flashback"); @@ -170,66 +58,4 @@ public class FlashbackAbility extends SpellAbility { sbRule.append(" (You may cast this card from your graveyard for its flashback cost. Then exile it.)"); return sbRule.toString(); } - - /** - * Used for split card in PlayerImpl method: - * getOtherUseableActivatedAbilities - * - * @param abilityName - */ - public FlashbackAbility setAbilityName(String abilityName) { - this.abilityName = abilityName; - return this; - } - -} - -class FlashbackReplacementEffect extends ReplacementEffectImpl { - - public FlashbackReplacementEffect() { - super(Duration.OneUse, Outcome.Exile); - staticText = "(If the flashback cost was paid, exile this card instead of putting it anywhere else any time it would leave the stack)"; - } - - protected FlashbackReplacementEffect(final FlashbackReplacementEffect effect) { - super(effect); - } - - @Override - public FlashbackReplacementEffect copy() { - return new FlashbackReplacementEffect(this); - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - Card card = game.getCard(event.getTargetId()); - if (card != null) { - discard(); - return controller.moveCards( - card, Zone.EXILED, source, game, false, false, false, event.getAppliedEffects()); - } - } - return false; - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ZONE_CHANGE; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - UUID cardId = CardUtil.getMainCardId(game, source.getSourceId()); // for split cards - if (cardId.equals(event.getTargetId()) - && ((ZoneChangeEvent) event).getFromZone() == Zone.STACK - && ((ZoneChangeEvent) event).getToZone() != Zone.EXILED) { - - int zcc = game.getState().getZoneChangeCounter(cardId); - return ((FixedTarget) getTargetPointer()).getZoneChangeCounter() + 1 == zcc; - - } - return false; - } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HarmonizeAbility.java b/Mage/src/main/java/mage/abilities/keyword/HarmonizeAbility.java index 92f6310401c..d18de35de8c 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HarmonizeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HarmonizeAbility.java @@ -1,19 +1,43 @@ package mage.abilities.keyword; +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; import mage.abilities.SpellAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.VariableCostImpl; +import mage.abilities.costs.VariableCostType; +import mage.abilities.costs.common.TapTargetCost; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.cost.CostModificationEffectImpl; import mage.cards.Card; -import mage.constants.Zone; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.permanent.PermanentIdPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetControlledPermanent; +import mage.util.CardUtil; + +import java.util.Objects; +import java.util.UUID; /** - * TODO: Implement this - * * @author TheElk801 */ -public class HarmonizeAbility extends SpellAbility { +public class HarmonizeAbility extends CastFromGraveyardAbility { + + private String abilityName; + private SpellAbility spellAbilityToResolve; public HarmonizeAbility(Card card, String manaString) { - super(new ManaCostsImpl<>(manaString), card.getName(), Zone.GRAVEYARD); + super(card, new ManaCostsImpl<>(manaString), SpellAbilityCastMode.HARMONIZE); + this.addCost(new HarmonizeCost()); + this.addSubAbility(new SimpleStaticAbility(Zone.ALL, new HarmonizeCostReductionEffect()).setRuleVisible(false)); } private HarmonizeAbility(final HarmonizeAbility ability) { @@ -24,4 +48,132 @@ public class HarmonizeAbility extends SpellAbility { public HarmonizeAbility copy() { return new HarmonizeAbility(this); } + + @Override + public String getRule() { + return name + " (You may cast this card from your graveyard for its harmonize cost. " + + "You may tap a creature you control to reduce that cost by {X}, " + + "where X is its power. Then exile this spell.)"; + } +} + +class HarmonizeCostReductionEffect extends CostModificationEffectImpl { + + HarmonizeCostReductionEffect() { + super(Duration.WhileOnStack, Outcome.Benefit, CostModificationType.REDUCE_COST); + } + + private HarmonizeCostReductionEffect(final HarmonizeCostReductionEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source, Ability abilityToModify) { + SpellAbility spellAbility = (SpellAbility) abilityToModify; + int power; + if (game.inCheckPlayableState()) { + power = game + .getBattlefield() + .getActivePermanents( + StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, + source.getControllerId(), source, game + ).stream() + .map(MageObject::getPower) + .mapToInt(MageInt::getValue) + .max() + .orElse(0); + } else { + power = CardUtil + .castStream(spellAbility.getCosts().stream(), HarmonizeCost.class) + .map(HarmonizeCost::getChosenCreature) + .map(game::getPermanent) + .filter(Objects::nonNull) + .map(MageObject::getPower) + .mapToInt(MageInt::getValue) + .map(x -> Math.max(x, 0)) + .sum(); + } + if (power > 0) { + CardUtil.adjustCost(spellAbility, power); + } + return true; + } + + @Override + public boolean applies(Ability abilityToModify, Ability source, Game game) { + return abilityToModify instanceof SpellAbility + && abilityToModify.getSourceId().equals(source.getSourceId()); + } + + @Override + public HarmonizeCostReductionEffect copy() { + return new HarmonizeCostReductionEffect(this); + } +} + +class HarmonizeCost extends VariableCostImpl { + + private UUID chosenCreature = null; + + HarmonizeCost() { + super(VariableCostType.ADDITIONAL, "", ""); + } + + private HarmonizeCost(final HarmonizeCost cost) { + super(cost); + this.chosenCreature = cost.chosenCreature; + } + + @Override + public HarmonizeCost copy() { + return new HarmonizeCost(this); + } + + @Override + public void clearPaid() { + super.clearPaid(); + chosenCreature = null; + } + + @Override + public int getMaxValue(Ability source, Game game) { + return game.getBattlefield().contains(StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, source, game, 1) ? 1 : 0; + } + + @Override + public int announceXValue(Ability source, Game game) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null || !game.getBattlefield().contains( + StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, source, game, 1 + ) || !player.chooseUse( + Outcome.Benefit, "Tap an untapped creature you control for harmonize?", source, game + )) { + return 0; + } + TargetPermanent target = new TargetPermanent(StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE); + target.withNotTarget(true); + target.withChooseHint("for harmonize"); + player.choose(Outcome.PlayForFree, target, source, game); + Permanent permanent = game.getPermanent(target.getFirstTarget()); + if (permanent == null) { + return 0; + } + chosenCreature = permanent.getId(); + return 1; + } + + private FilterControlledPermanent makeFilter() { + FilterControlledPermanent filter = new FilterControlledPermanent("tap the chosen creature"); + filter.add(new PermanentIdPredicate(chosenCreature)); + return filter; + } + + @Override + public Cost getFixedCostsFromAnnouncedValue(int xValue) { + return new TapTargetCost(new TargetControlledPermanent(xValue, xValue, makeFilter(), true)); + } + + public UUID getChosenCreature() { + return chosenCreature; + } } diff --git a/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java b/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java index a57e28325f6..6eedb18741f 100644 --- a/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java +++ b/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java @@ -15,6 +15,7 @@ public enum SpellAbilityCastMode { NORMAL("Normal"), MADNESS("Madness"), FLASHBACK("Flashback"), + HARMONIZE("Harmonize"), BESTOW("Bestow"), PROTOTYPE("Prototype"), MORPH("Morph", false, true), // and megamorph @@ -91,6 +92,7 @@ public enum SpellAbilityCastMode { case NORMAL: case MADNESS: case FLASHBACK: + case HARMONIZE: case DISTURB: case PLOT: case MORE_THAN_MEETS_THE_EYE: