mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 10:40:06 -08:00
[TDM] Implement harmonize mechanic (#13475)
* add initial ability * remove skip list * update harmonize ability * add reminder text * remove skip list * add harmonize test * update tests * create base class for flashback-like abilities
This commit is contained in:
parent
2f57cdb0a1
commit
4d8028adb9
7 changed files with 415 additions and 188 deletions
|
|
@ -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<String> 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()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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<Cost> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Cost> 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(" <i>(You may cast this card from your graveyard for its flashback cost. Then exile it.)</i>");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 + " <i>(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.)</i>";
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue