diff --git a/Mage.Sets/src/mage/cards/c/CosmosCharger.java b/Mage.Sets/src/mage/cards/c/CosmosCharger.java new file mode 100644 index 00000000000..db963fcb4f1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CosmosCharger.java @@ -0,0 +1,119 @@ +package mage.cards.c; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.common.cost.CostModificationEffectImpl; +import mage.abilities.keyword.FlashAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.ForetellAbility; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AsThoughEffectType; +import mage.constants.CardType; +import mage.constants.CostModificationType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.game.Game; +import mage.util.CardUtil; + +/** + * + * @author jeffwadsworth + */ +public final class CosmosCharger extends CardImpl { + + public CosmosCharger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}"); + + this.subtype.add(SubType.HORSE); + this.subtype.add(SubType.SPIRIT); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Foretelling cards from your hand costs {1} less and can be done on any player's turn. + this.addAbility(new SimpleStaticAbility(new CosmosChargerCostReductionEffect(ForetellAbility.class))); + this.addAbility(new SimpleStaticAbility(new CosmosChargerAllowForetellAnytime())); + + // Foretell 2U + this.addAbility(new ForetellAbility(this, "{2}{U}")); + + } + + private CosmosCharger(final CosmosCharger card) { + super(card); + } + + @Override + public CosmosCharger copy() { + return new CosmosCharger(this); + } +} + +class CosmosChargerCostReductionEffect extends CostModificationEffectImpl { + + private final Class specialAction; + + public CosmosChargerCostReductionEffect(Class specialAction) { + super(Duration.WhileOnBattlefield, Outcome.Neutral, CostModificationType.REDUCE_COST); + this.specialAction = specialAction; + staticText = "Foretelling cards from your hand costs {1} less and can be done on any player's turn"; + } + + public CosmosChargerCostReductionEffect(CosmosChargerCostReductionEffect effect) { + super(effect); + this.specialAction = effect.specialAction; + } + + @Override + public CosmosChargerCostReductionEffect copy() { + return new CosmosChargerCostReductionEffect(this); + } + + @Override + public boolean apply(Game game, Ability source, Ability abilityToModify) { + CardUtil.reduceCost(abilityToModify, 1); + return true; + } + + @Override + public boolean applies(Ability abilityToModify, Ability source, Game game) { + return abilityToModify.isControlledBy(source.getControllerId()) + && specialAction.isInstance(abilityToModify); + } +} + +class CosmosChargerAllowForetellAnytime extends AsThoughEffectImpl { + + public CosmosChargerAllowForetellAnytime() { + super(AsThoughEffectType.ALLOW_FORETELL_ANYTIME, Duration.WhileOnBattlefield, Outcome.Benefit); + } + + public CosmosChargerAllowForetellAnytime(final CosmosChargerAllowForetellAnytime effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public CosmosChargerAllowForetellAnytime copy() { + return new CosmosChargerAllowForetellAnytime(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/d/DreamDevourer.java b/Mage.Sets/src/mage/cards/d/DreamDevourer.java new file mode 100644 index 00000000000..25a6a41cd62 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DreamDevourer.java @@ -0,0 +1,98 @@ +package mage.cards.d; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.ForetellSourceControllerTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.keyword.ForetellAbility; +import mage.cards.Card; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Layer; +import mage.constants.Outcome; +import mage.constants.SubLayer; +import mage.constants.Zone; +import mage.filter.common.FilterNonlandCard; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.game.Game; +import mage.players.Player; +import mage.util.CardUtil; + +/** + * + * @author jeffwadsworth + */ +public final class DreamDevourer extends CardImpl { + + public DreamDevourer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}"); + + this.subtype.add(SubType.DEMON); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(0); + this.toughness = new MageInt(3); + + // Each nonland card in your hand without foretell has foretell. Its foretell cost is equal to its mana cost reduced by 2. + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new DreamDevourerAddAbilityEffect())); + + // Whenever you foretell a card, Dream Devourer gets +2/+0 until end of turn. + this.addAbility(new ForetellSourceControllerTriggeredAbility(new BoostSourceEffect(2, 0, Duration.EndOfTurn))); + + } + + private DreamDevourer(final DreamDevourer card) { + super(card); + } + + @Override + public DreamDevourer copy() { + return new DreamDevourer(this); + } +} + +class DreamDevourerAddAbilityEffect extends ContinuousEffectImpl { + + private static final FilterNonlandCard filter = new FilterNonlandCard(); + + static { + filter.add(Predicates.not(new AbilityPredicate(ForetellAbility.class))); + } + + DreamDevourerAddAbilityEffect() { + super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + this.staticText = "Each nonland card in your hand without foretell has foretell. Its foretell cost is equal to its mana cost reduced by 2"; + } + + private DreamDevourerAddAbilityEffect(final DreamDevourerAddAbilityEffect effect) { + super(effect); + } + + @Override + public DreamDevourerAddAbilityEffect copy() { + return new DreamDevourerAddAbilityEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + for (Card card : controller.getHand().getCards(filter, game)) { + String costText = CardUtil.reduceCost(card.getSpellAbility().getManaCostsToPay(), 2).getText(); + game.getState().setValue(card.getId().toString() + "Foretell Cost", costText); + ForetellAbility foretellAbility = new ForetellAbility(card, costText); + foretellAbility.setSourceId(card.getId()); + foretellAbility.setControllerId(card.getOwnerId()); + game.getState().addOtherAbility(card, foretellAbility); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/Kaldheim.java b/Mage.Sets/src/mage/sets/Kaldheim.java index 4eb7ef7bfbc..83e11fd7320 100644 --- a/Mage.Sets/src/mage/sets/Kaldheim.java +++ b/Mage.Sets/src/mage/sets/Kaldheim.java @@ -92,6 +92,7 @@ public final class Kaldheim extends ExpansionSet { cards.add(new SetCardInfo("Codespell Cleric", 7, Rarity.COMMON, mage.cards.c.CodespellCleric.class)); cards.add(new SetCardInfo("Colossal Plow", 236, Rarity.UNCOMMON, mage.cards.c.ColossalPlow.class)); cards.add(new SetCardInfo("Cosima, God of the Voyage", 50, Rarity.RARE, mage.cards.c.CosimaGodOfTheVoyage.class)); + cards.add(new SetCardInfo("Cosmos Charger", 51, Rarity.RARE, mage.cards.c.CosmosCharger.class)); cards.add(new SetCardInfo("Cosmos Elixir", 237, Rarity.RARE, mage.cards.c.CosmosElixir.class)); cards.add(new SetCardInfo("Craven Hulk", 127, Rarity.COMMON, mage.cards.c.CravenHulk.class)); cards.add(new SetCardInfo("Crippling Fear", 82, Rarity.RARE, mage.cards.c.CripplingFear.class)); @@ -114,6 +115,7 @@ public final class Kaldheim extends ExpansionSet { cards.add(new SetCardInfo("Draugr Thought-Thief", 55, Rarity.COMMON, mage.cards.d.DraugrThoughtThief.class)); cards.add(new SetCardInfo("Draugr's Helm", 88, Rarity.UNCOMMON, mage.cards.d.DraugrsHelm.class)); cards.add(new SetCardInfo("Dread Rider", 89, Rarity.COMMON, mage.cards.d.DreadRider.class)); + cards.add(new SetCardInfo("Dream Devourer", 352, Rarity.RARE, mage.cards.d.DreamDevourer.class)); cards.add(new SetCardInfo("Dual Strike", 132, Rarity.UNCOMMON, mage.cards.d.DualStrike.class)); cards.add(new SetCardInfo("Duskwielder", 91, Rarity.COMMON, mage.cards.d.Duskwielder.class)); cards.add(new SetCardInfo("Dwarven Hammer", 133, Rarity.UNCOMMON, mage.cards.d.DwarvenHammer.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/KarfellHarbingerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/KarfellHarbingerTest.java index ad10e99c2d1..92994e8ece7 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/KarfellHarbingerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/KarfellHarbingerTest.java @@ -2,20 +2,23 @@ package org.mage.test.cards.single.khm; import mage.constants.PhaseStep; import mage.constants.Zone; +import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** * @author TheElk801 */ + public class KarfellHarbingerTest extends CardTestPlayerBase { - @Test + @Ignore // unignore when we find a better ability text for foretell public void testForetellMana() { addCard(Zone.BATTLEFIELD, playerA, "Island", 1); addCard(Zone.BATTLEFIELD, playerA, "Karfell Harbinger"); addCard(Zone.HAND, playerA, "Augury Raven"); + //activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{2}: Foretold this card."); activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{2}: Foretold this card."); setStrictChooseMode(true); diff --git a/Mage/src/main/java/mage/abilities/common/ForetellSourceControllerTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/ForetellSourceControllerTriggeredAbility.java new file mode 100644 index 00000000000..a130ae0df57 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/ForetellSourceControllerTriggeredAbility.java @@ -0,0 +1,70 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package mage.abilities.common; + +import mage.abilities.Ability; +import mage.abilities.SpecialAction; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.cards.Card; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.players.Player; + +/** + * + * @author jeffwadsworth + */ +public class ForetellSourceControllerTriggeredAbility extends TriggeredAbilityImpl { + + public ForetellSourceControllerTriggeredAbility(Effect effect) { + super(Zone.BATTLEFIELD, effect, false); + } + + public ForetellSourceControllerTriggeredAbility(final ForetellSourceControllerTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.TAKEN_SPECIAL_ACTION; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + //UUID specialAction = event.getTargetId(); + Card card = game.getCard(event.getSourceId()); + Player player = game.getPlayer(event.getPlayerId()); + for (Ability a : card.getAbilities()) { + if (player.getId() == controllerId + && (a instanceof SpecialAction) + && a.getRule().endsWith("and exile this card from your hand face down. Cast it on a later turn for its foretell cost.)")) { + return true; + } + } + // if the ability is added to cards via effect + for (Ability a : game.getState().getAllOtherAbilities(card.getId())) { + if (player.getId() == controllerId + && (a instanceof SpecialAction) + && a.getRule().endsWith("and exile this card from your hand face down. Cast it on a later turn for its foretell cost.)")) { + return true; + } + } + return false; + } + + @Override + public String getRule() { + return "Whenever you foretell a card, " + super.getRule(); + } + + @Override + public ForetellSourceControllerTriggeredAbility copy() { + return new ForetellSourceControllerTriggeredAbility(this); + } + +} diff --git a/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java b/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java index 30bcd274e3c..213b8607c5d 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java @@ -2,6 +2,7 @@ package mage.abilities.keyword; import java.util.UUID; import mage.MageObject; +import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.SpecialAction; import mage.abilities.SpellAbility; @@ -11,6 +12,7 @@ import mage.abilities.costs.Costs; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ExileTargetEffect; @@ -19,9 +21,11 @@ import mage.cards.ModalDoubleFacesCard; import mage.cards.SplitCard; import mage.constants.AsThoughEffectType; import mage.constants.Duration; +import mage.constants.Layer; import mage.constants.Outcome; import mage.constants.SpellAbilityCastMode; import mage.constants.SpellAbilityType; +import mage.constants.SubLayer; import mage.constants.Zone; import mage.game.ExileZone; import mage.game.Game; @@ -44,13 +48,11 @@ public class ForetellAbility extends SpecialAction { this.card = card; this.usesStack = Boolean.FALSE; this.addCost(new GenericManaCost(2)); - // exile the card - this.addEffect(new ForetellExileEffect(card)); - // foretell cost from exile : it can't be any other cost - addSubAbility(new ForetellCostAbility(foretellCost)); + // exile the card and it can't be cast the turn it was foretold + this.addEffect(new ForetellExileEffect(card, foretellCost)); // look at face-down card anytime addSubAbility(new SimpleStaticAbility(Zone.ALL, new ForetellLookAtCardEffect())); - this.setRuleVisible(false); + this.setRuleVisible(true); this.addWatcher(new ForetoldWatcher()); } @@ -68,13 +70,69 @@ public class ForetellAbility extends SpecialAction { @Override public ActivationStatus canActivate(UUID playerId, Game game) { // activate only during the controller's turn - if (!game.isActivePlayer(this.getControllerId())) { + if (game.getState().getContinuousEffects().getApplicableAsThoughEffects(AsThoughEffectType.ALLOW_FORETELL_ANYTIME, game).isEmpty() + && !game.isActivePlayer(this.getControllerId())) { return ActivationStatus.getFalse(); } return super.canActivate(playerId, game); } } +class ForetellExileEffect extends OneShotEffect { + + private Card card; + String foretellCost; + + public ForetellExileEffect(Card card, String foretellCost) { + super(Outcome.Neutral); + this.card = card; + this.foretellCost = foretellCost; + StringBuilder sbRule = new StringBuilder("Foretell"); + sbRule.append("—"); + sbRule.append(foretellCost); + sbRule.append(" (During your turn, you may pay {2} and exile this card from your hand face down. Cast it on a later turn for its foretell cost.)"); + staticText = sbRule.toString(); + } + + public ForetellExileEffect(final ForetellExileEffect effect) { + super(effect); + this.card = effect.card; + this.foretellCost = effect.foretellCost; + } + + @Override + public ForetellExileEffect copy() { + return new ForetellExileEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null + && card != null) { + // retrieve the exileId of the foretold card + UUID exileId = CardUtil.getExileZoneId(card.getId().toString() + "foretellAbility", game); + + // foretell turn number shows up on exile window + Effect effect = new ExileTargetEffect(exileId, " Foretell Turn Number: " + game.getTurnNum()); + + // remember turn number it was cast + game.getState().setValue(card.getId().toString() + "Foretell Turn Number", game.getTurnNum()); + + // remember the foretell cost + game.getState().setValue(card.getId().toString() + "Foretell Cost", foretellCost); + + // exile the card face-down + effect.setTargetPointer(new FixedTarget(card.getId())); + effect.apply(game, source); + card.setFaceDown(true, game); + game.addEffect(new ForetellAddCostEffect(new MageObjectReference(card, game)), source); + return true; + } + return false; + } +} + class ForetellLookAtCardEffect extends AsThoughEffectImpl { public ForetellLookAtCardEffect() { @@ -115,42 +173,41 @@ class ForetellLookAtCardEffect extends AsThoughEffectImpl { } } -class ForetellExileEffect extends OneShotEffect { +// this needed to be a continuousEffect for a card like Dream Devourer...unless someone has a better idea +class ForetellAddCostEffect extends ContinuousEffectImpl { - private Card card; + private final MageObjectReference mor; - public ForetellExileEffect(Card card) { - super(Outcome.Neutral); - this.card = card; - staticText = "Foretold this card"; + public ForetellAddCostEffect(MageObjectReference mor) { + super(Duration.EndOfGame, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + this.mor = mor; + staticText = "Foretold card"; } - public ForetellExileEffect(final ForetellExileEffect effect) { + public ForetellAddCostEffect(final ForetellAddCostEffect effect) { super(effect); - this.card = effect.card; - } - - @Override - public ForetellExileEffect copy() { - return new ForetellExileEffect(this); + this.mor = effect.mor; } @Override public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null - && card != null) { - UUID exileId = CardUtil.getExileZoneId(card.getId().toString() + "foretellAbility", game); - // foretell turn number shows up on exile window - Effect effect = new ExileTargetEffect(exileId, " Foretell Turn Number: " + game.getTurnNum()); - // remember turn number it was cast - game.getState().setValue(card.getId().toString() + "Foretell Turn Number", game.getTurnNum()); - effect.setTargetPointer(new FixedTarget(card.getId())); - effect.apply(game, source); - card.setFaceDown(true, game); - return true; + Card card = mor.getCard(game); + if (card != null + && game.getState().getZone(card.getId()) == Zone.EXILED) { + String foretellCost = (String) game.getState().getValue(card.getId().toString() + "Foretell Cost"); + Ability ability = new ForetellCostAbility(foretellCost); + ability.setSourceId(card.getId()); + ability.setControllerId(source.getControllerId()); + game.getState().addOtherAbility(card, ability); + } else { + discard(); } - return false; + return true; + } + + @Override + public ForetellAddCostEffect copy() { + return new ForetellAddCostEffect(this); } } @@ -160,7 +217,7 @@ class ForetellCostAbility extends SpellAbility { private SpellAbility spellAbilityToResolve; public ForetellCostAbility(String foretellCost) { - super(null, "Foretell", Zone.EXILED, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.NORMAL); + super(null, "Testing", Zone.EXILED, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.NORMAL); this.setAdditionalCostsRuleVisible(false); this.name = "Foretell " + foretellCost; this.addCost(new ManaCostsImpl(foretellCost)); @@ -268,33 +325,7 @@ class ForetellCostAbility extends SpellAbility { @Override public String getRule(boolean all) { - return this.getRule(); - } - - @Override - public String getRule() { - StringBuilder sbRule = new StringBuilder("Foretell"); - if (!costs.isEmpty()) { - sbRule.append("—"); - } else { - sbRule.append(' '); - } - if (!manaCosts.isEmpty()) { - sbRule.append(manaCosts.getText()); - } - if (!costs.isEmpty()) { - if (!manaCosts.isEmpty()) { - sbRule.append(", "); - } - sbRule.append(costs.getText()); - sbRule.append('.'); - } - if (abilityName != null) { - sbRule.append(' '); - sbRule.append(abilityName); - } - sbRule.append(" (During your turn, you may pay {2} and exile this card from your hand face down. Cast it on a later turn for its foretell cost.)"); - return sbRule.toString(); + return ""; } /** diff --git a/Mage/src/main/java/mage/constants/AsThoughEffectType.java b/Mage/src/main/java/mage/constants/AsThoughEffectType.java index c4f1ace53b0..74b8a27f59f 100644 --- a/Mage/src/main/java/mage/constants/AsThoughEffectType.java +++ b/Mage/src/main/java/mage/constants/AsThoughEffectType.java @@ -18,7 +18,6 @@ public enum AsThoughEffectType { BLOCK_FORESTWALK, DAMAGE_NOT_BLOCKED, BE_BLOCKED, - // PLAY_FROM_NOT_OWN_HAND_ZONE + CAST_AS_INSTANT: // 1. Do not use dialogs in "applies" method for that type of effect (it calls multiple times and will freeze the game) // 2. All effects in "applies" must checks affectedControllerId.equals(source.getControllerId()) (if not then all players will be able to play it) @@ -26,23 +25,22 @@ public enum AsThoughEffectType { // TODO: search all PLAY_FROM_NOT_OWN_HAND_ZONE and CAST_AS_INSTANT effects and add support of mainCard and objectId PLAY_FROM_NOT_OWN_HAND_ZONE(true), CAST_AS_INSTANT(true), - ACTIVATE_AS_INSTANT, DAMAGE, SHROUD, HEXPROOF, PAY_0_ECHO, LOOK_AT_FACE_DOWN, - // SPEND_OTHER_MANA: // 1. It's uses for mana calcs at any zone, not stack only // 2. Compare zone change counter as "objectZCC <= targetZCC + 1" // 3. Compare zone with original (like exiled) and stack, not stack only // TODO: search all SPEND_ONLY_MANA effects and improve counters compare as SPEND_OTHER_MANA SPEND_OTHER_MANA, - SPEND_ONLY_MANA, - TARGET; + TARGET, + // Cosmos Charger effect + ALLOW_FORETELL_ANYTIME; private final boolean needPlayCardAbility; // mark effect type as compatible with play/cast abilities