From cf2319bf8100fa754bcb416e3aa0cabf1b15e0f0 Mon Sep 17 00:00:00 2001 From: karapuzz14 Date: Mon, 23 Sep 2024 17:54:46 +0300 Subject: [PATCH 01/12] Perpetually mechanic prototype with almost all J21 perpetually cards and few perpetually cards from another sets. --- .../sources/ScryfallImageSupportTokens.java | 4 + .../src/main/java/mage/view/CardView.java | 2 +- Mage.Sets/src/mage/cards/a/ArdenAngel.java | 2 +- .../src/mage/cards/b/BafflingDefenses.java | 42 ++ .../src/mage/cards/b/BenalishPartisan.java | 56 +++ .../src/mage/cards/b/BloodsproutTalisman.java | 44 ++ .../src/mage/cards/d/DavrielSoulBroker.java | 433 ++++++++++++++++++ .../src/mage/cards/d/DavrielsWithering.java | 44 ++ Mage.Sets/src/mage/cards/e/EtherealGrasp.java | 79 ++++ .../cards/f/FreyaliseSkyshroudPartisan.java | 116 +++++ .../src/mage/cards/l/LeoninSanctifier.java | 44 ++ .../src/mage/cards/l/LongtuskStalker.java | 105 +++++ .../mage/cards/l/LumberingLightshield.java | 100 ++++ .../src/mage/cards/m/ManagorgerPhoenix.java | 109 +++++ .../src/mage/cards/m/MentorOfEvosIsle.java | 44 ++ Mage.Sets/src/mage/cards/p/PainfulBond.java | 41 ++ .../mage/cards/p/PlaguecraftersFamiliar.java | 39 ++ .../src/mage/cards/p/PullOfTheMistMoon.java | 65 +++ .../src/mage/cards/r/RecklessRingleader.java | 44 ++ .../mage/cards/s/SarkhanWandererToShiv.java | 126 +++++ Mage.Sets/src/mage/cards/s/ScionOfShiv.java | 43 ++ .../src/mage/cards/t/TeyoAegisAdept.java | 87 ++++ .../src/mage/cards/v/VeteranCharger.java | 89 ++++ Mage.Sets/src/mage/sets/AlchemyDominaria.java | 2 + Mage.Sets/src/mage/sets/AlchemyKamigawa.java | 1 + .../mage/sets/JumpstartHistoricHorizons.java | 19 +- .../test/cards/digital/PerpetuallyTest.java | 83 ++++ .../CycleControllerTriggeredAbility.java | 5 +- .../abilities/effects/ContinuousEffects.java | 39 ++ .../effects/ContinuousEffectsList.java | 1 + .../abilities/effects/PerpetuallyEffect.java | 14 + .../common/BoostSourcePerpetuallyEffect.java | 61 +++ .../CardsInYourHandPerpetuallyGainEffect.java | 67 +++ ...ardInYourHandItPerpetuallyGainsEffect.java | 65 +++ .../GainAbilitySourcePerpetuallyEffect.java | 50 ++ .../abilities/effects/common/MeldEffect.java | 20 + .../BoostTargetPerpetuallyEffect.java | 234 ++++++++++ .../CommanderReplacementEffect.java | 13 + .../GainAbilityTargetPerpetuallyEffect.java | 206 +++++++++ ...PowerToughnessTargetPerpetuallyEffect.java | 176 +++++++ .../cost/SpellCostIncreaseSourceEffect.java | 2 +- .../main/java/mage/constants/Duration.java | 1 + Mage/src/main/java/mage/game/GameState.java | 7 +- ...DavrielSoulBrokerBoostCreaturesEmblem.java | 36 ++ ...oulBrokerBoostOpponentCreaturesEmblem.java | 36 ++ .../DavrielSoulBrokerCostIncreaseEmblem.java | 36 ++ .../DavrielSoulBrokerCostReductionEmblem.java | 33 ++ .../DavrielSoulBrokerGainLifeEmblem.java | 26 ++ ...rielSoulBrokerPlaneswalkersBuffEmblem.java | 41 ++ ...DavrielSoulBrokerTriggeredExileEmblem.java | 30 ++ ...rielSoulBrokerTriggeredLoseLifeEmblem.java | 35 ++ .../command/emblems/TeyoAegisAdeptEmblem.java | 77 ++++ .../main/java/mage/players/PlayerImpl.java | 9 + Mage/src/main/java/mage/util/CardUtil.java | 45 +- Mage/src/main/resources/tokens-database.txt | 11 + 55 files changed, 3231 insertions(+), 8 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/b/BafflingDefenses.java create mode 100644 Mage.Sets/src/mage/cards/b/BenalishPartisan.java create mode 100644 Mage.Sets/src/mage/cards/b/BloodsproutTalisman.java create mode 100644 Mage.Sets/src/mage/cards/d/DavrielSoulBroker.java create mode 100644 Mage.Sets/src/mage/cards/d/DavrielsWithering.java create mode 100644 Mage.Sets/src/mage/cards/e/EtherealGrasp.java create mode 100644 Mage.Sets/src/mage/cards/f/FreyaliseSkyshroudPartisan.java create mode 100644 Mage.Sets/src/mage/cards/l/LeoninSanctifier.java create mode 100644 Mage.Sets/src/mage/cards/l/LongtuskStalker.java create mode 100644 Mage.Sets/src/mage/cards/l/LumberingLightshield.java create mode 100644 Mage.Sets/src/mage/cards/m/ManagorgerPhoenix.java create mode 100644 Mage.Sets/src/mage/cards/m/MentorOfEvosIsle.java create mode 100644 Mage.Sets/src/mage/cards/p/PainfulBond.java create mode 100644 Mage.Sets/src/mage/cards/p/PlaguecraftersFamiliar.java create mode 100644 Mage.Sets/src/mage/cards/p/PullOfTheMistMoon.java create mode 100644 Mage.Sets/src/mage/cards/r/RecklessRingleader.java create mode 100644 Mage.Sets/src/mage/cards/s/SarkhanWandererToShiv.java create mode 100644 Mage.Sets/src/mage/cards/s/ScionOfShiv.java create mode 100644 Mage.Sets/src/mage/cards/t/TeyoAegisAdept.java create mode 100644 Mage.Sets/src/mage/cards/v/VeteranCharger.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java create mode 100644 Mage/src/main/java/mage/abilities/effects/PerpetuallyEffect.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/BoostSourcePerpetuallyEffect.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/CardsInYourHandPerpetuallyGainEffect.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/ChooseACardInYourHandItPerpetuallyGainsEffect.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/GainAbilitySourcePerpetuallyEffect.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/continuous/BoostTargetPerpetuallyEffect.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetPerpetuallyEffect.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/continuous/SetBasePowerToughnessTargetPerpetuallyEffect.java create mode 100644 Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerBoostCreaturesEmblem.java create mode 100644 Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerBoostOpponentCreaturesEmblem.java create mode 100644 Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerCostIncreaseEmblem.java create mode 100644 Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerCostReductionEmblem.java create mode 100644 Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerGainLifeEmblem.java create mode 100644 Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerPlaneswalkersBuffEmblem.java create mode 100644 Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerTriggeredExileEmblem.java create mode 100644 Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerTriggeredLoseLifeEmblem.java create mode 100644 Mage/src/main/java/mage/game/command/emblems/TeyoAegisAdeptEmblem.java diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java index 28d7dbf6f3f1..51a16c9a59c2 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java @@ -681,6 +681,10 @@ public class ScryfallImageSupportTokens { put("AFC/Servo", "https://api.scryfall.com/cards/tafc/11/en?format=image"); put("AFC/Thopter", "https://api.scryfall.com/cards/tafc/12/en?format=image"); + // J21 + put("J21/Emblem Davriel", "https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg"); + put("J21/Emblem Teyo", "https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg"); + // MIC put("MIC/Beast", "https://api.scryfall.com/cards/tmic/7/en?format=image"); put("MIC/Centaur", "https://api.scryfall.com/cards/tmic/8/en?format=image"); diff --git a/Mage.Common/src/main/java/mage/view/CardView.java b/Mage.Common/src/main/java/mage/view/CardView.java index 1462b395b354..d0209e12d909 100644 --- a/Mage.Common/src/main/java/mage/view/CardView.java +++ b/Mage.Common/src/main/java/mage/view/CardView.java @@ -372,7 +372,7 @@ public CardView(Card sourceCard, Game game, boolean showAsControlled, boolean st this.color = card.getColor(null).copy(); this.superTypes = new ArrayList<>(card.getSuperType()); this.subTypes = card.getSubtype().copy(); - this.rules = new ArrayList<>(card.getRules()); + this.rules = new ArrayList<>(card.getRules(game)); } // GUI: enable day/night button to view original face up card diff --git a/Mage.Sets/src/mage/cards/a/ArdenAngel.java b/Mage.Sets/src/mage/cards/a/ArdenAngel.java index 42f06cc0430f..31c01e3ebb8b 100644 --- a/Mage.Sets/src/mage/cards/a/ArdenAngel.java +++ b/Mage.Sets/src/mage/cards/a/ArdenAngel.java @@ -33,7 +33,7 @@ public ArdenAngel(UUID ownerId, CardSetInfo setInfo) { // At the beginning of your upkeep, if Arden Angel is in your graveyard, roll a four-sided die. If the result is 1, return Arden Angel from your graveyard to the battlefield. this.addAbility(new ConditionalInterveningIfTriggeredAbility( - new BeginningOfUpkeepTriggeredAbility(new ArdenAngelEffect(), TargetController.YOU, false), + new BeginningOfUpkeepTriggeredAbility(Zone.GRAVEYARD, new ArdenAngelEffect(), TargetController.YOU, false), SourceInGraveyardCondition.instance, "At the beginning of your upkeep, if {this} is in your graveyard, " + "roll a four-sided die. If the result is 1, return {this} from your graveyard to the battlefield." )); diff --git a/Mage.Sets/src/mage/cards/b/BafflingDefenses.java b/Mage.Sets/src/mage/cards/b/BafflingDefenses.java new file mode 100644 index 000000000000..73cd7de673f5 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BafflingDefenses.java @@ -0,0 +1,42 @@ +package mage.cards.b; + +import java.util.UUID; + +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.continuous.BecomesCreatureTargetEffect; +import mage.abilities.effects.common.continuous.SetBasePowerToughnessTargetPerpetuallyEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.game.permanent.token.FrogToken; +import mage.target.common.TargetCreaturePermanent; + +/** + * + * @author karapuzz14 + */ +public final class BafflingDefenses extends CardImpl { + + public BafflingDefenses(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{W}"); + + + // Target creature's base power perpetually becomes 0. + Effect effect = new SetBasePowerToughnessTargetPerpetuallyEffect(StaticValue.get(0), null); + effect.setText("Target creature’s base power perpetually becomes 0"); + this.getSpellAbility().addEffect(effect); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + + } + + private BafflingDefenses(final BafflingDefenses card) { + super(card); + } + + @Override + public BafflingDefenses copy() { + return new BafflingDefenses(this); + } +} diff --git a/Mage.Sets/src/mage/cards/b/BenalishPartisan.java b/Mage.Sets/src/mage/cards/b/BenalishPartisan.java new file mode 100644 index 000000000000..7e25fe514f1b --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BenalishPartisan.java @@ -0,0 +1,56 @@ +package mage.cards.b; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.CycleControllerTriggeredAbility; +import mage.abilities.effects.common.BoostSourcePerpetuallyEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.ReturnSourceFromGraveyardToBattlefieldEffect; +import mage.constants.SubType; +import mage.abilities.keyword.LifelinkAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.keyword.CyclingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Zone; + +/** + * + * @author karapuzz14 + */ +public final class BenalishPartisan extends CardImpl { + + public BenalishPartisan(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(1); + this.toughness = new MageInt(2); + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + // Whenever you cycle another card, you may pay {1}{W}. If you do, return Benalish Partisan from your graveyard to the battlefield tapped and it perpetually gets +1/+0. + DoIfCostPaid effect = new DoIfCostPaid( + new ReturnSourceFromGraveyardToBattlefieldEffect(true), + new ManaCostsImpl<>("{1}{W}")); + effect.addEffect(new BoostSourcePerpetuallyEffect(1, 0).setText(" and it perpetually gets +1/0")); + + this.addAbility(new CycleControllerTriggeredAbility(Zone.GRAVEYARD, effect, false, true)); + + // Cycling {1}{W} + this.addAbility(new CyclingAbility(new ManaCostsImpl<>("{1}{W}"))); + + } + + private BenalishPartisan(final BenalishPartisan card) { + super(card); + } + + @Override + public BenalishPartisan copy() { + return new BenalishPartisan(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/b/BloodsproutTalisman.java b/Mage.Sets/src/mage/cards/b/BloodsproutTalisman.java new file mode 100644 index 000000000000..16267a8aa894 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BloodsproutTalisman.java @@ -0,0 +1,44 @@ +package mage.cards.b; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTappedAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.effects.common.ChooseACardInYourHandItPerpetuallyGainsEffect; +import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Zone; +import mage.filter.StaticFilters; + +import java.util.UUID; + +public class BloodsproutTalisman extends CardImpl { + + public BloodsproutTalisman(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{B}{G}"); + + // Bloodsprout Talisman enters tapped. + this.addAbility(new EntersBattlefieldTappedAbility()); + + // {T}, Pay 1 life: Choose a nonland card in your hand. It perpetually gains “This spell costs {1} less to cast.” + Ability spellCostReductionAbility = new SimpleStaticAbility(Zone.ALL, new SpellCostReductionSourceEffect(1)); + Ability ability = new SimpleActivatedAbility( + new ChooseACardInYourHandItPerpetuallyGainsEffect(spellCostReductionAbility, StaticFilters.FILTER_CARD_A_NON_LAND), + new TapSourceCost()); + ability.addCost(new PayLifeCost(1)); + this.addAbility(ability); + } + + private BloodsproutTalisman(final BloodsproutTalisman card) { + super(card); + } + + @Override + public BloodsproutTalisman copy() { + return new BloodsproutTalisman(this); + } +} diff --git a/Mage.Sets/src/mage/cards/d/DavrielSoulBroker.java b/Mage.Sets/src/mage/cards/d/DavrielSoulBroker.java new file mode 100644 index 000000000000..2b604a1d6824 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DavrielSoulBroker.java @@ -0,0 +1,433 @@ +package mage.cards.d; + +import java.util.*; +import java.util.stream.Collectors; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.delayed.UntilYourNextTurnDelayedTriggeredAbility; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.Effect; +import mage.abilities.effects.Effects; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.*; +import mage.abilities.effects.common.continuous.BoostTargetPerpetuallyEffect; +import mage.cards.*; +import mage.choices.Choice; +import mage.choices.ChoiceHintType; +import mage.choices.ChoiceImpl; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; +import mage.game.command.emblems.*; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetOpponentsCreaturePermanent; +import mage.target.common.TargetSacrifice; +import mage.target.targetpointer.FixedTarget; +import mage.target.targetpointer.FixedTargets; +import mage.target.targetpointer.TargetPointer; +import mage.util.CardUtil; +import mage.util.RandomUtil; + +/** + * + * @author karapuzz14 + */ +public final class DavrielSoulBroker extends CardImpl { + + public DavrielSoulBroker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{2}{B}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.DAVRIEL); + this.setStartingLoyalty(4); + + // +1: Until your next turn, whenever an opponent attacks you and/or planeswalkers you control, they discard a card. If they can't, they sacrifice an attacking creature. + this.addAbility(new LoyaltyAbility(new CreateDelayedTriggeredAbilityEffect( + new UntilYourNextTurnDelayedTriggeredAbility( + new DavrielSoulBrokerTriggeredAbility() + ) + ), 1)); + + // −2: Accept one of Davriel's offers, then accept one of Davriel's conditions. + this.addAbility(new LoyaltyAbility(new DavrielSoulBrokerAcceptOffersAndConditionsEffect(), -2)); + + // −3: Target creature an opponent controls perpetually gets -3/-3. + ContinuousEffect effect = new BoostTargetPerpetuallyEffect(-3, -3); + effect.setText("Target creature an opponent controls perpetually gets -3/-3"); + + Ability ability = new LoyaltyAbility(effect, -3); + ability.addTarget(new TargetOpponentsCreaturePermanent()); + this.addAbility(ability); + } + + private DavrielSoulBroker(final DavrielSoulBroker card) { + super(card); + } + + @Override + public DavrielSoulBroker copy() { + return new DavrielSoulBroker(this); + } +} + +class DavrielSoulBrokerTriggeredAbility extends TriggeredAbilityImpl { + + public DavrielSoulBrokerTriggeredAbility() { + super(Zone.BATTLEFIELD, new DavrielSoulBrokerFirstEffect(), false); + } + + private DavrielSoulBrokerTriggeredAbility(final DavrielSoulBrokerTriggeredAbility ability) { + super(ability); + } + + @Override + public DavrielSoulBrokerTriggeredAbility copy() { + return new DavrielSoulBrokerTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DECLARED_ATTACKERS; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + UUID playerId = game.getCombat() + .getAttackers() + .stream() + .filter(attacker -> isControlledBy(game.getCombat().getDefendingPlayerId(attacker, game))) + .map(game::getControllerId) + .filter(game.getOpponents(getControllerId())::contains) + .findFirst() + .orElse(null); + if (playerId == null) { + return false; + } + getEffects().setTargetPointer(new FixedTarget(playerId)); + return true; + } + + @Override + public String getRule() { + return "Whenever an opponent attacks you and/or planeswalkers you control, " + + "they discard a card. If they can't, they sacrifice an attacking creature"; + } +} + +class DavrielSoulBrokerFirstEffect extends OneShotEffect { + + DavrielSoulBrokerFirstEffect() { + super(Outcome.Discard); + } + + private DavrielSoulBrokerFirstEffect(final DavrielSoulBrokerFirstEffect effect) { + super(effect); + } + + @Override + public DavrielSoulBrokerFirstEffect copy() { + return new DavrielSoulBrokerFirstEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + int affectedTargets = 0; + for(UUID target : getTargetPointer().getTargets(game, source)) { + Player opponent = game.getPlayer(target); + if (opponent == null) { + continue; + } + // they discard a card + Cards discardedCards = opponent.discard(1, false, false, source, game); + + // If they can't, they sacrifice an attacking creature. + if (discardedCards == null || discardedCards.isEmpty()) { + TargetSacrifice targetSacrifice = new TargetSacrifice(StaticFilters.FILTER_AN_ATTACKING_CREATURE); + if (targetSacrifice.canChoose(opponent.getId(), source, game)) { + opponent.choose(Outcome.Sacrifice, targetSacrifice, source, game); + + Permanent permanent = game.getPermanent(targetSacrifice.getFirstTarget()); + if (permanent != null && permanent.sacrifice(source, game)) { + affectedTargets++; + } + } + } else { + affectedTargets++; + } + } + return affectedTargets > 0; + } + +} + +class DavrielSoulBrokerAcceptOffersAndConditionsEffect extends OneShotEffect { + + private Effects offers = new Effects(); + private Effects conditions = new Effects(); + + public DavrielSoulBrokerAcceptOffersAndConditionsEffect() { + super(Outcome.Neutral); + staticText = "Accept one of Davriel's offers, then accept one of Davriel's conditions."; + + // Offers and conditions list source: https://mtg.fandom.com/wiki/Davriel_Cane/Davriel%27s_Contracts + offers.add(new DrawCardSourceControllerEffect(3)); // DONE + offers.add(new ConjureCardEffect("Manor Guardian")); + offers.add(new DavrielReturnFromGraveyardOfferEffect()); + + FilterCard filter = new FilterCreatureCard(); + filter.add(HighestValueAmongCreatureCardsInYourGraveyardPredicate.instance); + //offers.add(new ReturnFromGraveyardAtRandomEffect(filter, Zone.BATTLEFIELD) + // .setText("Return a random creature card " + + // "with the highest mana value from among cards in your graveyard to the battlefield")); +// + //offers.add(new GetEmblemEffect(new DavrielSoulBrokerBoostCreaturesEmblem())); // DONE + //offers.add(new GetEmblemEffect(new DavrielSoulBrokerCostReductionEmblem())); + //offers.add(new GetEmblemEffect(new DavrielSoulBrokerPlaneswalkersBuffEmblem())); + //offers.add(new GetEmblemEffect(new DavrielSoulBrokerGainLifeEmblem())); + + conditions.add(new LoseLifeSourceControllerEffect(6)); // DONE + conditions.add(new DavrielExileConditionEffect()); + //conditions.add(new SacrificeControllerEffect(StaticFilters.FILTER_PERMANENTS, 2, "")); // DONE + conditions.add(new DavrielBoostOpponentCreaturesConditionEffect()); + //conditions.add(new GetEmblemEffect(new DavrielSoulBrokerBoostOpponentCreaturesEmblem())); + //conditions.add(new GetEmblemEffect(new DavrielSoulBrokerCostIncreaseEmblem())); // DONE + //conditions.add(new GetEmblemEffect(new DavrielSoulBrokerTriggeredExileEmblem())); // DONE + //conditions.add(new GetEmblemEffect(new DavrielSoulBrokerTriggeredLoseLifeEmblem())); // DONE + + + + } + + private DavrielSoulBrokerAcceptOffersAndConditionsEffect(final DavrielSoulBrokerAcceptOffersAndConditionsEffect effect) { + super(effect); + this.offers = effect.offers.copy(); + this.conditions = effect.conditions.copy(); + + } + + @Override + public DavrielSoulBrokerAcceptOffersAndConditionsEffect copy() { + return new DavrielSoulBrokerAcceptOffersAndConditionsEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + + Effect chosenOffer = chooseOfferOrCondition(true, player, game); + if(chosenOffer != null) { + chosenOffer.apply(game, source); + } + else { + return false; + } + + Effect chosenCondition = chooseOfferOrCondition(false, player, game); + if(chosenCondition != null) { + chosenCondition.apply(game, source); + } + else { + return false; + } + + return true; + } + + private Effect chooseOfferOrCondition(boolean isOffer, Player player, Game game) { + Set toSelect = new HashSet<>(); + while (toSelect.size() < 3) { + if (isOffer) { + toSelect.add(RandomUtil.randomFromCollection(offers)); + } + else { + toSelect.add(RandomUtil.randomFromCollection(conditions)); + } + } + Choice choice = new ChoiceImpl(true, ChoiceHintType.TEXT); + String choiceMessage = "Accept one of Davriel's "; + + if(isOffer) { + choiceMessage += "offers"; + } + else { + choiceMessage += "conditions"; + } + choice.setMessage(choiceMessage); + + Map effectRuleMap = new HashMap<>(); + for(Effect effect : toSelect) { + if(effect instanceof OneShotEffect) { + effectRuleMap.put(CardUtil.getTextWithFirstCharUpperCase(effect.getText(null)), effect); + } + } + choice.setChoices(new LinkedHashSet<>(effectRuleMap.keySet())); + player.choose(outcome, choice, game); + String effectRule = choice.getChoice(); + if (effectRule == null) { + return null; + } + return effectRuleMap.get(effectRule); + } +} + +enum HighestValueAmongCreatureCardsInYourGraveyardPredicate implements ObjectSourcePlayerPredicate { + instance; + + private static final FilterCard filter = new FilterCreatureCard(); + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + Player player = game.getPlayer(input.getPlayerId()); + int cmc = player.getGraveyard().getCards(filter, game) + .stream() + .filter(Objects::nonNull) + .mapToInt(MageObject::getManaValue) + .max() + .orElse(0); + return input.getObject().getManaValue() >= cmc; + } +} + +class DavrielExileConditionEffect extends OneShotEffect { + + DavrielExileConditionEffect() { + super(Outcome.Detriment); + staticText = "Exile two cards from your hand. " + + "If fewer than two cards were exiled this way, " + + "each opponent draws cards equal to the difference"; + } + + private DavrielExileConditionEffect(final DavrielExileConditionEffect effect) { + super(effect); + } + + @Override + public DavrielExileConditionEffect copy() { + return new DavrielExileConditionEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if(controller == null) { + return false; + } + int exiledCardsCount = 0; + if (!controller.getHand().isEmpty()) { + TargetCard target = new TargetCard(2, 2, Zone.HAND, new FilterCard()); + if (controller.choose(Outcome.Exile, controller.getHand(), target, source, game)) { + List cards = target.getTargets().stream() + .map(game::getCard) + .collect(Collectors.toList()); + + exiledCardsCount = cards.size(); + Cards cardsToExile = new CardsImpl(); + cardsToExile.addAllCards(cards); + controller.moveCards(cardsToExile, Zone.EXILED, source, game); + } + } + + if(exiledCardsCount < 2) { + Set opponents = game.getOpponents(source.getControllerId()); + for(UUID id : opponents) { + Player opponent = game.getPlayer(id); + opponent.drawCards(2 - exiledCardsCount, source, game); + } + } + + return true; + } +} + +class DavrielReturnFromGraveyardOfferEffect extends OneShotEffect { + + DavrielReturnFromGraveyardOfferEffect() { + super(Outcome.ReturnToHand); + staticText = "Return two random creature cards from your graveyard to your hand. They perpetually get +1/+1"; + } + + private DavrielReturnFromGraveyardOfferEffect(final DavrielReturnFromGraveyardOfferEffect effect) { + super(effect); + } + + @Override + public DavrielReturnFromGraveyardOfferEffect copy() { + return new DavrielReturnFromGraveyardOfferEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + Set creatureCards = controller.getGraveyard().getCards(new FilterCreatureCard(), game); + Cards randomCards = new CardsImpl(); + for(int i = 0; i < 2; i++) { + Card randomCard = RandomUtil.randomFromCollection(creatureCards); + randomCards.add(randomCard); + creatureCards.remove(randomCard); + } + + randomCards.retainZone(Zone.GRAVEYARD, game); //verify the target card is still in the graveyard + if (randomCards.isEmpty() || !controller.moveCards(randomCards, Zone.HAND, source, game)) { + return false; + } + + TargetPointer pointer = new FixedTargets(randomCards.getCards(game), game); + BoostTargetPerpetuallyEffect effect = new BoostTargetPerpetuallyEffect(1, 1); + effect.setTargetPointer(pointer); + game.addEffect(effect, source); + + return true; + } +} + +class DavrielBoostOpponentCreaturesConditionEffect extends OneShotEffect { + + DavrielBoostOpponentCreaturesConditionEffect() { + super(Outcome.Detriment); + staticText = "Each creature you don't control perpetually gets +1/+1"; + } + + private DavrielBoostOpponentCreaturesConditionEffect(final DavrielBoostOpponentCreaturesConditionEffect effect) { + super(effect); + } + + @Override + public DavrielBoostOpponentCreaturesConditionEffect copy() { + return new DavrielBoostOpponentCreaturesConditionEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + List targets = game.getBattlefield().getActivePermanents( + StaticFilters.FILTER_CREATURES_YOU_DONT_CONTROL, source.getControllerId(), source, game); + if(targets == null || targets.isEmpty()) { + return false; + } + + TargetPointer pointer = new FixedTargets(targets, game); + BoostTargetPerpetuallyEffect effect = new BoostTargetPerpetuallyEffect(1, 1); + effect.setTargetPointer(pointer); + game.addEffect(effect, source); + + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/d/DavrielsWithering.java b/Mage.Sets/src/mage/cards/d/DavrielsWithering.java new file mode 100644 index 000000000000..efb9dff33f55 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DavrielsWithering.java @@ -0,0 +1,44 @@ +package mage.cards.d; + +import java.util.UUID; + +import mage.ObjectColor; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.continuous.BecomesColorTargetEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.continuous.BoostTargetPerpetuallyEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.target.common.TargetCreaturePermanent; +import mage.target.common.TargetOpponentsCreaturePermanent; + +/** + * + * @author karapuzz14 + */ +public final class DavrielsWithering extends CardImpl { + + public DavrielsWithering(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{B}"); + + + // Target creature an opponent controls perpetually gets -1/-2. + this.getSpellAbility().addTarget(new TargetOpponentsCreaturePermanent()); + + ContinuousEffect effect = new BoostTargetPerpetuallyEffect(-1, -2); + effect.setText("Target creature an opponent controls perpetually gets -1/-2"); + this.getSpellAbility().addEffect(effect); + } + + private DavrielsWithering(final DavrielsWithering card) { + super(card); + } + + @Override + public DavrielsWithering copy() { + return new DavrielsWithering(this); + } +} diff --git a/Mage.Sets/src/mage/cards/e/EtherealGrasp.java b/Mage.Sets/src/mage/cards/e/EtherealGrasp.java new file mode 100644 index 000000000000..bdda8ee011ab --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EtherealGrasp.java @@ -0,0 +1,79 @@ + +package mage.cards.e; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.*; +import mage.abilities.effects.common.continuous.GainAbilityTargetPerpetuallyEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.target.common.TargetCreaturePermanent; +import mage.target.targetpointer.FixedTarget; + +/** + * @author noxx + */ +public final class EtherealGrasp extends CardImpl { + + public EtherealGrasp(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{U}"); + + // Tap target creature. + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + this.getSpellAbility().addEffect(new TapTargetEffect()); + + // That creature perpetually gains “This creature doesn’t untap during your untap step” and “{8}: Untap this creature.” + this.getSpellAbility().addEffect(new EtherealGraspEffect()); + } + + private EtherealGrasp(final EtherealGrasp card) { + super(card); + } + + @Override + public EtherealGrasp copy() { + return new EtherealGrasp(this); + } +} + +class EtherealGraspEffect extends OneShotEffect { + + EtherealGraspEffect() { + super(Outcome.AddAbility); + this.staticText = "That creature perpetually gains “This creature doesn’t untap during your untap step” and “{8}: Untap this creature.”"; + } + + private EtherealGraspEffect(final EtherealGraspEffect effect) { + super(effect); + } + + @Override + public EtherealGraspEffect copy() { + return new EtherealGraspEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + + game.addEffect(new GainAbilityTargetPerpetuallyEffect( + new SimpleStaticAbility(Zone.BATTLEFIELD, new DontUntapInControllersUntapStepSourceEffect()), + "creature perpetually gains “This creature doesn’t untap during your untap step”" + ).setTargetPointer(new FixedTarget(source.getFirstTarget(), game)), source); + + game.addEffect(new GainAbilityTargetPerpetuallyEffect( + new SimpleActivatedAbility(Zone.BATTLEFIELD, new UntapSourceEffect(), new GenericManaCost(8)) + ).setTargetPointer(new FixedTarget(source.getFirstTarget(), game)), source); + + return true; + } +} + diff --git a/Mage.Sets/src/mage/cards/f/FreyaliseSkyshroudPartisan.java b/Mage.Sets/src/mage/cards/f/FreyaliseSkyshroudPartisan.java new file mode 100644 index 000000000000..2f94ae5be810 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FreyaliseSkyshroudPartisan.java @@ -0,0 +1,116 @@ +package mage.cards.f; + +import java.util.Collection; +import java.util.Objects; +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ConjureCardEffect; +import mage.abilities.effects.common.SeekCardEffect; +import mage.abilities.effects.common.continuous.BoostTargetPerpetuallyEffect; +import mage.cards.Card; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; + +import mage.filter.FilterPermanent; +import mage.filter.common.FilterBySubtypeCard; +import mage.filter.common.FilterCreatureCard; +import mage.game.Game; +import mage.players.Player; +import mage.target.Target; +import mage.target.TargetPermanent; +import mage.target.targetpointer.FixedTarget; +import mage.util.RandomUtil; + +/** + * + * @author karapuzz14 + */ +public final class FreyaliseSkyshroudPartisan extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent(); + + static { + filter.add(SubType.ELF.getPredicate()); + } + + public FreyaliseSkyshroudPartisan(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{1}{G}{G}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.FREYALISE); + this.setStartingLoyalty(4); + + // +1: Untap up to one target Elf. That Elf and a random Elf creature card in your hand perpetually get +1/+1. + LoyaltyAbility untapAbility = new LoyaltyAbility(new FreyaliseUntapEffect(), 1); + untapAbility.addTarget(new TargetPermanent(0, 1, filter, false)); + this.addAbility(untapAbility); + + // −1: Seek an Elf card. + LoyaltyAbility seekAbility = new LoyaltyAbility(new SeekCardEffect(new FilterBySubtypeCard(SubType.ELF)), -1); + this.addAbility(seekAbility); + + // −6: Conjure a Regal Force card onto the battlefield. + LoyaltyAbility conjureAbility = new LoyaltyAbility( + new ConjureCardEffect("Regal Force", Zone.BATTLEFIELD, 1), -6); + this.addAbility(conjureAbility); + } + + private FreyaliseSkyshroudPartisan(final FreyaliseSkyshroudPartisan card) { + super(card); + } + + @Override + public FreyaliseSkyshroudPartisan copy() { + return new FreyaliseSkyshroudPartisan(this); + } +} + +class FreyaliseUntapEffect extends OneShotEffect { + + private static final FilterCreatureCard filter = new FilterCreatureCard(); + + static { + filter.add(SubType.ELF.getPredicate()); + } + FreyaliseUntapEffect() { + super(Outcome.Untap); + this.staticText = "Untap up to one target Elf. That Elf and a random Elf creature card in your hand perpetually get +1/+1."; + } + + private FreyaliseUntapEffect(final FreyaliseUntapEffect effect) { + super(effect); + } + + @Override + public FreyaliseUntapEffect copy() { + return new FreyaliseUntapEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + source.getTargets() + .stream() + .map(Target::getTargets) + .flatMap(Collection::stream) + .map(game::getPermanent) + .filter(Objects::nonNull) + .forEachOrdered(permanent -> { + permanent.untap(game); + game.addEffect(new BoostTargetPerpetuallyEffect(1, 1).setTargetPointer(new FixedTarget(permanent, game)), source); + }); + + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null && !controller.getHand().isEmpty()) { + Card cardFromHand = RandomUtil.randomFromCollection(controller.getHand().getCards(filter, game)); + if (cardFromHand == null) { + return false; + } + game.addEffect(new BoostTargetPerpetuallyEffect(1, 1).setTargetPointer(new FixedTarget(cardFromHand, game)), source); + } + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/l/LeoninSanctifier.java b/Mage.Sets/src/mage/cards/l/LeoninSanctifier.java new file mode 100644 index 000000000000..684fe1cb1af3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LeoninSanctifier.java @@ -0,0 +1,44 @@ +package mage.cards.l; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.ChooseACardInYourHandItPerpetuallyGainsEffect; +import mage.constants.SubType; +import mage.abilities.keyword.LifelinkAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.common.FilterCreatureCard; + +/** + * + * @author karapuzz14 + */ +public final class LeoninSanctifier extends CardImpl { + + public LeoninSanctifier(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); + + this.subtype.add(SubType.CAT); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + // When Leonin Sanctifier enters the battlefield, choose a creature card in your hand. It perpetually gains lifelink. + this.addAbility(new EntersBattlefieldTriggeredAbility( + new ChooseACardInYourHandItPerpetuallyGainsEffect(LifelinkAbility.getInstance(), new FilterCreatureCard()))); + } + + private LeoninSanctifier(final LeoninSanctifier card) { + super(card); + } + + @Override + public LeoninSanctifier copy() { + return new LeoninSanctifier(this); + } +} diff --git a/Mage.Sets/src/mage/cards/l/LongtuskStalker.java b/Mage.Sets/src/mage/cards/l/LongtuskStalker.java new file mode 100644 index 000000000000..3ebf76720f93 --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LongtuskStalker.java @@ -0,0 +1,105 @@ +package mage.cards.l; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.PayEnergyCost; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.BoostSourcePerpetuallyEffect; +import mage.abilities.effects.common.continuous.BoostTargetPerpetuallyEffect; +import mage.abilities.effects.common.counter.GetEnergyCountersControllerEffect; +import mage.cards.Card; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.filter.common.FilterCreatureCard; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInHand; +import mage.target.targetpointer.FixedTarget; +import mage.util.RandomUtil; + +/** + * + * @author karapuzz + */ +public final class LongtuskStalker extends CardImpl { + + public LongtuskStalker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}"); + + this.subtype.add(SubType.CAT); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Whenever Longtusk Stalker enters the battlefield or attacks, you get {E}. + this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new GetEnergyCountersControllerEffect(1), false)); + + // Pay {E}{E}: Longtusk Stalker perpetually gets +1/+0. You may choose a creature card in your hand. If you do, that card perpetually gets +1/+0. + this.addAbility(new SimpleActivatedAbility( + new LogtuskAbilityEffect(), new PayEnergyCost(2) + )); + } + + private LongtuskStalker(final LongtuskStalker card) { + super(card); + } + + @Override + public LongtuskStalker copy() { + return new LongtuskStalker(this); + } +} + +class LogtuskAbilityEffect extends OneShotEffect { + + LogtuskAbilityEffect() { + super(Outcome.BoostCreature); + this.staticText = "{this} perpetually gets +1/+0. You may choose a creature card in your hand. If you do, that card perpetually gets +1/+0."; + } + + private LogtuskAbilityEffect(final LogtuskAbilityEffect effect) { + super(effect); + } + + @Override + public LogtuskAbilityEffect copy() { + return new LogtuskAbilityEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + Effect boostLongtusk = new BoostSourcePerpetuallyEffect(1, 0); + boostLongtusk.apply(game, source); + + if (!controller.getHand().isEmpty()) { + Card cardFromHand = null; + if (controller.getHand().size() > 1) { + TargetCard target = new TargetCardInHand(0, 1, new FilterCreatureCard()); + if (controller.choose(Outcome.BoostCreature, controller.getHand(), target, source, game)) { + cardFromHand = game.getCard(target.getFirstTarget()); + } + } else { + cardFromHand = RandomUtil.randomFromCollection(controller.getHand().getCards(new FilterCreatureCard(), game)); + } + + if (cardFromHand == null) { + return false; + } + + game.addEffect(new BoostTargetPerpetuallyEffect(1, 0) + .setTargetPointer(new FixedTarget(cardFromHand, game)), source); + return true; + } + } + return false; + } + +} + diff --git a/Mage.Sets/src/mage/cards/l/LumberingLightshield.java b/Mage.Sets/src/mage/cards/l/LumberingLightshield.java new file mode 100644 index 000000000000..763f7daab33b --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LumberingLightshield.java @@ -0,0 +1,100 @@ +package mage.cards.l; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetPerpetuallyEffect; +import mage.abilities.effects.common.cost.SpellCostIncreaseSourceEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.cards.*; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreatureCard; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInHand; +import mage.target.common.TargetOpponent; +import mage.target.targetpointer.FixedTarget; +import mage.util.RandomUtil; + +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public final class LumberingLightshield extends CardImpl { + + public LumberingLightshield(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); + + this.subtype.add(SubType.ILLUSION); + this.power = new MageInt(1); + this.toughness = new MageInt(4); + + // When Lumbering Lightshield enters, target opponent reveals a nonland card at random from their hand. It perpetually gains “This spell costs {1} more to cast.” + Ability ability = new EntersBattlefieldTriggeredAbility(new LumberingLightshieldEffect()); + ability.addTarget(new TargetOpponent()); + this.addAbility(ability); + + } + + private LumberingLightshield(final LumberingLightshield card) { + super(card); + } + + @Override + public LumberingLightshield copy() { + return new LumberingLightshield(this); + } +} + +class LumberingLightshieldEffect extends OneShotEffect { + + LumberingLightshieldEffect() { + super(Outcome.AddAbility); + this.staticText = "target opponent reveals a nonland card at random from their hand. It perpetually gains “This spell costs {1} more to cast.”"; + } + + private LumberingLightshieldEffect(final LumberingLightshieldEffect effect) { + super(effect); + } + + @Override + public LumberingLightshieldEffect copy() { + return new LumberingLightshieldEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player opponent = game.getPlayer(getTargetPointer().getFirst(game, source)); + if (opponent != null && !opponent.getHand().isEmpty()) { + Cards revealed = new CardsImpl(); + + Set cards = opponent.getHand() + .getCards(game) + .stream() + .filter(card -> StaticFilters.FILTER_CARD_NON_LAND.match(card, getId(), source, game)) + .collect(Collectors.toSet()); + + Card card = RandomUtil.randomFromCollection(cards); + if (card != null) { + revealed.add(card); + opponent.revealCards("Lumbering Lightshield", revealed, game); + + game.addEffect(new GainAbilityTargetPerpetuallyEffect( + new SimpleStaticAbility( + Zone.ALL, new SpellCostIncreaseSourceEffect(1) + ) + ).setTargetPointer(new FixedTarget(card, game)), source); + } + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/cards/m/ManagorgerPhoenix.java b/Mage.Sets/src/mage/cards/m/ManagorgerPhoenix.java new file mode 100644 index 000000000000..9eabc3a92551 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/ManagorgerPhoenix.java @@ -0,0 +1,109 @@ +package mage.cards.m; + +import java.util.UUID; +import mage.MageInt; +import mage.MageObjectReference; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.common.CantBlockAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.condition.common.SourceInGraveyardCondition; +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.ReturnToBattlefieldUnderOwnerControlSourceEffect; +import mage.abilities.effects.common.ReturnToBattlefieldUnderOwnerControlTargetEffect; +import mage.abilities.effects.common.continuous.BoostTargetPerpetuallyEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.Card; +import mage.constants.*; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.counters.CounterType; +import mage.filter.FilterSpell; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; + +/** + * + * @author karapuzz14 + */ +//TODO: learn how to improve targeting inside effect? +public final class ManagorgerPhoenix extends CardImpl { + + public ManagorgerPhoenix(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}{R}"); + + this.subtype.add(SubType.PHOENIX); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Managorger Phoenix can't block. + this.addAbility(new CantBlockAbility()); + + // Whenever you cast a spell, if Managorger Phoenix is in your graveyard, put a flame counter on Managorger Phoenix for each {R} in that spell's mana cost. If Managorger Phoenix has five or more flame counters on it, return it to the battlefield and it perpetually gets +1/+1. + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new SpellCastControllerTriggeredAbility(Zone.GRAVEYARD, new ManagorgerPhoenixEffect(), new FilterSpell(), false, SetTargetPointer.SPELL), + SourceInGraveyardCondition.instance, "Whenever you cast a spell, if Managorger Phoenix is in your graveyard," + + " put a flame counter on Managorger Phoenix for each {R} in that spell's mana cost. " + + "If Managorger Phoenix has five or more flame counters on it, " + + "return it to the battlefield and it perpetually gets +1/+1." + )); + } + + private ManagorgerPhoenix(final ManagorgerPhoenix card) { + super(card); + } + + @Override + public ManagorgerPhoenix copy() { + return new ManagorgerPhoenix(this); + } +} +class ManagorgerPhoenixEffect extends OneShotEffect { + + ManagorgerPhoenixEffect() { + super(Outcome.Benefit); + } + + private ManagorgerPhoenixEffect(final ManagorgerPhoenixEffect effect) { + super(effect); + } + + @Override + public ManagorgerPhoenixEffect copy() { + return new ManagorgerPhoenixEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Spell spell = (Spell) this.getValue("spellCast"); + if(controller != null && spell != null) { + + int redManaCount = spell.getManaCost().getMana().getRed(); + AddCountersSourceEffect effect = new AddCountersSourceEffect(CounterType.FLAME.createInstance(), StaticValue.get(redManaCount), true, true); + effect.apply(game, source); + + Card card = game.getCard(source.getSourceId()); + if (card.getCounters(game).getCount("flame") >= 5) { + ReturnToBattlefieldUnderOwnerControlTargetEffect returnEffect = new ReturnToBattlefieldUnderOwnerControlTargetEffect(false, false); + returnEffect.setTargetPointer(new FixedTarget(card.getId())); + returnEffect.apply(game, source); + + game.addEffect(new BoostTargetPerpetuallyEffect(1, 0).setTargetPointer(new FixedTarget(card.getId())), source); + } + return true; + } + return false; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/m/MentorOfEvosIsle.java b/Mage.Sets/src/mage/cards/m/MentorOfEvosIsle.java new file mode 100644 index 000000000000..6b2d3e555305 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MentorOfEvosIsle.java @@ -0,0 +1,44 @@ +package mage.cards.m; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.ChooseACardInYourHandItPerpetuallyGainsEffect; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.common.FilterCreatureCard; + +/** + * + * @author anonymous + */ +public final class MentorOfEvosIsle extends CardImpl { + + public MentorOfEvosIsle(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}"); + + this.subtype.add(SubType.BIRD); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When Mentor of Evos Isle enters the battlefield, choose a creature card in your hand. It perpetually gains flying. + this.addAbility(new EntersBattlefieldTriggeredAbility( + new ChooseACardInYourHandItPerpetuallyGainsEffect(FlyingAbility.getInstance(), new FilterCreatureCard()))); + } + + private MentorOfEvosIsle(final MentorOfEvosIsle card) { + super(card); + } + + @Override + public MentorOfEvosIsle copy() { + return new MentorOfEvosIsle(this); + } +} diff --git a/Mage.Sets/src/mage/cards/p/PainfulBond.java b/Mage.Sets/src/mage/cards/p/PainfulBond.java new file mode 100644 index 000000000000..8f2ff15bb312 --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PainfulBond.java @@ -0,0 +1,41 @@ +package mage.cards.p; + +import mage.abilities.Ability; +import mage.abilities.effects.common.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.filter.FilterCard; +import mage.filter.common.FilterNonlandCard; +import mage.filter.predicate.mageobject.ManaValuePredicate; + +import java.util.UUID; + +public final class PainfulBond extends CardImpl { + + private static final FilterCard filter = new FilterNonlandCard(" then nonland cards in your hand with mana value 3 or less"); + + static { + filter.add(new ManaValuePredicate(ComparisonType.OR_LESS, 3)); + } + + public PainfulBond(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{B}"); + + // Draw two cards, then nonland cards in your hand with mana value 3 or less perpetually gain “When you cast this spell, you lose 1 life.” + getSpellAbility().addEffect(new DrawCardSourceControllerEffect(2) + .setText("Draw two cards,")); + Ability loseLifeAbility = new CastSourceTriggeredAbility(new LoseLifeSourceControllerEffect(1), false); + getSpellAbility().addEffect(new CardsInYourHandPerpetuallyGainEffect(loseLifeAbility, filter)); + } + + private PainfulBond(final PainfulBond card) { + super(card); + } + + @Override + public PainfulBond copy() { + return new PainfulBond(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/p/PlaguecraftersFamiliar.java b/Mage.Sets/src/mage/cards/p/PlaguecraftersFamiliar.java new file mode 100644 index 000000000000..a21609ea7b0d --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PlaguecraftersFamiliar.java @@ -0,0 +1,39 @@ +package mage.cards.p; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.ChooseACardInYourHandItPerpetuallyGainsEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.common.FilterCreatureCard; +import java.util.UUID; + +public final class PlaguecraftersFamiliar extends CardImpl { + + public PlaguecraftersFamiliar(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}"); + + this.subtype.add(SubType.RAT); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Deathtouch + this.addAbility(DeathtouchAbility.getInstance()); + + // When Plaguecrafter’s Familiar enters the battlefield, choose a creature card in your hand. It perpetually gains deathtouch. + this.addAbility(new EntersBattlefieldTriggeredAbility( + new ChooseACardInYourHandItPerpetuallyGainsEffect(DeathtouchAbility.getInstance(), new FilterCreatureCard()))); + } + + private PlaguecraftersFamiliar(final PlaguecraftersFamiliar card) { + super(card); + } + + @Override + public PlaguecraftersFamiliar copy() { + return new PlaguecraftersFamiliar(this); + } +} diff --git a/Mage.Sets/src/mage/cards/p/PullOfTheMistMoon.java b/Mage.Sets/src/mage/cards/p/PullOfTheMistMoon.java new file mode 100644 index 000000000000..b5191540d76c --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PullOfTheMistMoon.java @@ -0,0 +1,65 @@ +package mage.cards.p; + +import java.util.UUID; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.KickedCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.ExileUntilSourceLeavesEffect; +import mage.abilities.effects.common.ChooseACardInYourHandItPerpetuallyGainsEffect; +import mage.abilities.keyword.KickerAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.StaticFilters; +import mage.filter.common.FilterPermanentCard; +import mage.filter.predicate.Predicates; +import mage.target.TargetPermanent; + + +/** + * + * @author karapuzz14 + */ +public final class PullOfTheMistMoon extends CardImpl { + + public PullOfTheMistMoon(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{W}{W}"); + + // Kicker {1}{U} + this.addAbility(new KickerAbility("{1}{U}")); + + // When Pull of the Mist Moon enters, exile target nonland permanent an opponent controls + // until Pull of the Mist Moon leaves the battlefield. + Ability ability = new EntersBattlefieldTriggeredAbility(new ExileUntilSourceLeavesEffect()); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_NON_LAND)); + this.addAbility(ability); + + // When Pull of the Mist Moon enters, if it was kicked, choose a nonland permanent card in your hand. + // It perpetually gains “When this permanent enters, exile target nonland permanent an opponent controls + // until this permanent leaves the battlefield.” + Ability exileAbility = new EntersBattlefieldTriggeredAbility(new ExileUntilSourceLeavesEffect()); + exileAbility.addTarget(new TargetPermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_NON_LAND)); + + FilterPermanentCard filter = new FilterPermanentCard(); + filter.add(Predicates.not(CardType.LAND.getPredicate())); + + //TODO: text autogeneration? + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new EntersBattlefieldTriggeredAbility(new ChooseACardInYourHandItPerpetuallyGainsEffect(exileAbility, filter)), KickedCondition.ONCE, + "When {this} enters, if it was kicked, choose a nonland permanent card in your hand. " + + "It perpetually gains \"When this permanent enters, exile target nonland permanent an opponent controls" + + " until this permanent leaves the battlefield.\"" + )); + } + + private PullOfTheMistMoon(final PullOfTheMistMoon card) { + super(card); + } + + @Override + public PullOfTheMistMoon copy() { + return new PullOfTheMistMoon(this); + } +} + diff --git a/Mage.Sets/src/mage/cards/r/RecklessRingleader.java b/Mage.Sets/src/mage/cards/r/RecklessRingleader.java new file mode 100644 index 000000000000..d5fa00556a75 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RecklessRingleader.java @@ -0,0 +1,44 @@ +package mage.cards.r; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.ChooseACardInYourHandItPerpetuallyGainsEffect; +import mage.constants.SubType; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.common.FilterCreatureCard; + +/** + * + * @author anonymous + */ +public final class RecklessRingleader extends CardImpl { + + public RecklessRingleader(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}"); + + this.subtype.add(SubType.GOBLIN); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Haste + this.addAbility(HasteAbility.getInstance()); + + // When Reckless Ringleader enters the battlefield, choose a creature card in your hand. It perpetually gains haste. + this.addAbility(new EntersBattlefieldTriggeredAbility( + new ChooseACardInYourHandItPerpetuallyGainsEffect(HasteAbility.getInstance(), new FilterCreatureCard()))); + } + + private RecklessRingleader(final RecklessRingleader card) { + super(card); + } + + @Override + public RecklessRingleader copy() { + return new RecklessRingleader(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SarkhanWandererToShiv.java b/Mage.Sets/src/mage/cards/s/SarkhanWandererToShiv.java new file mode 100644 index 000000000000..f25b42decc3a --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SarkhanWandererToShiv.java @@ -0,0 +1,126 @@ +package mage.cards.s; + +import java.util.*; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.SourceIsSpellCondition; +import mage.abilities.costs.AlternativeCostSourceAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.DynamicCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ConjureCardEffect; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetPerpetuallyEffect; +import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; +import mage.cards.Card; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCreaturePermanent; +import mage.target.targetpointer.FixedTargets; +import mage.target.targetpointer.TargetPointer; + +/** + * + * @author karapuzz14 + */ +public final class SarkhanWandererToShiv extends CardImpl { + + public SarkhanWandererToShiv(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{3}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SARKHAN); + this.setStartingLoyalty(4); + + // +1: Dragon cards in your hand perpetually gain "This spell costs {1} less to cast," and "You may pay {X} rather than pay this spell's mana cost, where X is its mana value." + LoyaltyAbility perpetuallyAbility = new LoyaltyAbility(new SarkhanWandererToShivEffect(), 1); + this.addAbility(perpetuallyAbility); + + // +1: Conjure a Shivan Dragon card into your hand. + LoyaltyAbility conjureAbility = new LoyaltyAbility(new ConjureCardEffect("Shivan Dragon"), 1); + this.addAbility(conjureAbility); + + // −2: Sarkhan, Wanderer to Shiv deals 3 damage to target creature. + LoyaltyAbility damageAbility = new LoyaltyAbility(new DamageTargetEffect(3), -2); + damageAbility.addTarget(new TargetCreaturePermanent()); + this.addAbility(damageAbility); + } + + private SarkhanWandererToShiv(final SarkhanWandererToShiv card) { + super(card); + } + + @Override + public SarkhanWandererToShiv copy() { + return new SarkhanWandererToShiv(this); + } +} + +class SarkhanWandererToShivEffect extends OneShotEffect { + + private final AlternativeCostSourceAbility alternativeCastingCostAbility = new AlternativeCostSourceAbility( + SourceIsSpellCondition.instance, "You may pay {X} rather than pay this spell's mana cost, where X is its mana value.", + new FilterCard(), true, new ColorlessManaValue()); + private static final FilterCard filter = new FilterCard(); + + static { + filter.add(SubType.DRAGON.getPredicate()); + } + SarkhanWandererToShivEffect() { + super(Outcome.Benefit); + this.staticText = "Dragon cards in your hand perpetually gain \"This spell costs {1} less to cast\", and \"You may pay {X} rather than pay this spell's mana cost, where X is its mana value.\""; + } + + private SarkhanWandererToShivEffect(final SarkhanWandererToShivEffect effect) { + super(effect); + } + + @Override + public SarkhanWandererToShivEffect copy() { + return new SarkhanWandererToShivEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + + if (controller != null && !controller.getHand().isEmpty()) { + Set dragonCards = controller.getHand().getCards(filter, game); + if (dragonCards == null) { + return false; + } + TargetPointer pointer = new FixedTargets(dragonCards, game); + + ContinuousEffect altCostEffect = new GainAbilityTargetPerpetuallyEffect(alternativeCastingCostAbility.setRuleAtTheTop(false)).setTargetPointer(pointer); + game.addEffect(altCostEffect, source); + + ContinuousEffect reduceCostEffect = new GainAbilityTargetPerpetuallyEffect(new SimpleStaticAbility( + Zone.ALL, new SpellCostReductionSourceEffect(1) + )).setTargetPointer(pointer); + game.addEffect(reduceCostEffect, source); + + } + return true; + } +} + +class ColorlessManaValue implements DynamicCost { + + @Override + public Cost getCost(Ability ability, Game game) { + return new GenericManaCost(ability.getManaCosts().manaValue()); + } + + @Override + public String getText(Ability ability, Game game) { + return "Pay " + getCost(ability, game).getText() + " rather than " + ability.getManaCosts().getText() + " for " + ability.getSourceObject(game).getIdName() + "?"; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/ScionOfShiv.java b/Mage.Sets/src/mage/cards/s/ScionOfShiv.java new file mode 100644 index 000000000000..9e4c4d22b934 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/ScionOfShiv.java @@ -0,0 +1,43 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.BoostSourcePerpetuallyEffect; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Zone; + +/** + * + * @author karapuzz14 + */ +public final class ScionOfShiv extends CardImpl { + + public ScionOfShiv(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{R}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // {2}{R}: Scion of Shiv perpetually gets +1/+0. + this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new BoostSourcePerpetuallyEffect(1,0), new ManaCostsImpl<>("{2}{R}"))); + } + + private ScionOfShiv(final ScionOfShiv card) { + super(card); + } + + @Override + public ScionOfShiv copy() { + return new ScionOfShiv(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TeyoAegisAdept.java b/Mage.Sets/src/mage/cards/t/TeyoAegisAdept.java new file mode 100644 index 000000000000..509ec00c4371 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TeyoAegisAdept.java @@ -0,0 +1,87 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ConjureCardEffect; +import mage.abilities.effects.common.GetEmblemEffect; +import mage.abilities.effects.common.combat.CanAttackAsThoughItDidntHaveDefenderSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetPerpetuallyEffect; +import mage.abilities.effects.common.continuous.SetBasePowerToughnessTargetPerpetuallyEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.command.emblems.TeyoAegisAdeptEmblem; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * + * @author karapuzz14 + */ +// TODO: исправить GainAbility при выборе нескольких целей последовательно +public final class TeyoAegisAdept extends CardImpl { + + public TeyoAegisAdept(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{2}{W}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.TEYO); + this.setStartingLoyalty(4); + + // +1: Up to one target creature’s base power perpetually becomes equal to its toughness. It perpetually gains “This creature can attack as though it didn’t have defender.” + Ability firstLoyaltyAbility = new LoyaltyAbility(new TeyoAegisAdeptFirstEffect(), 1); + Ability canAttackAbility = new SimpleStaticAbility(Zone.ALL, new CanAttackAsThoughItDidntHaveDefenderSourceEffect(Duration.WhileOnBattlefield)); + firstLoyaltyAbility.addEffect(new GainAbilityTargetPerpetuallyEffect(canAttackAbility) + .setText("It perpetually gains \"This creature can attack as though it didn’t have defender.\"")); + firstLoyaltyAbility.addTarget(new TargetCreaturePermanent(0, 1)); + this.addAbility(firstLoyaltyAbility); + + // −2: Conjure a card named Lumbering Lightshield onto the battlefield. + this.addAbility(new LoyaltyAbility(new ConjureCardEffect("Lumbering Lightshield", Zone.BATTLEFIELD, 1), -2)); + + // −6: You get an emblem with “At the beginning of your end step, return target white creature card from your graveyard to the battlefield. You gain life equal to its toughness.” + this.addAbility(new LoyaltyAbility(new GetEmblemEffect(new TeyoAegisAdeptEmblem()), -6)); + } + + private TeyoAegisAdept(final TeyoAegisAdept card) { + super(card); + } + + @Override + public TeyoAegisAdept copy() { + return new TeyoAegisAdept(this); + } +} + +class TeyoAegisAdeptFirstEffect extends OneShotEffect { + + TeyoAegisAdeptFirstEffect() { + super(Outcome.BoostCreature); + staticText = "Up to one target creature’s base power perpetually becomes equal to its toughness"; + } + + private TeyoAegisAdeptFirstEffect(final TeyoAegisAdeptFirstEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (permanent != null) { + game.addEffect(new SetBasePowerToughnessTargetPerpetuallyEffect(StaticValue.get(permanent.getToughness().getValue()), null), source); + } + return false; + } + + @Override + public TeyoAegisAdeptFirstEffect copy() { + return new TeyoAegisAdeptFirstEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/v/VeteranCharger.java b/Mage.Sets/src/mage/cards/v/VeteranCharger.java new file mode 100644 index 000000000000..abd64a9b4b4b --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VeteranCharger.java @@ -0,0 +1,89 @@ +package mage.cards.v; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.BoostTargetPerpetuallyEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.common.FilterCreatureCard; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInHand; +import mage.target.targetpointer.FixedTarget; +import mage.util.RandomUtil; + +import java.util.UUID; + +public final class VeteranCharger extends CardImpl { + + public VeteranCharger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.subtype.add(SubType.CENTAUR); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // When Veteran Charger enters the battlefield, choose a creature card in your hand. It perpetually gets +2/+2. + this.addAbility(new EntersBattlefieldTriggeredAbility(new VeteranChargerEffect())); + } + + private VeteranCharger(final VeteranCharger card) { + super(card); + } + + @Override + public VeteranCharger copy() { + return new VeteranCharger(this); + } +} + +class VeteranChargerEffect extends OneShotEffect { + + VeteranChargerEffect() { + super(Outcome.AddAbility); + this.staticText = "choose a creature card in your hand. It perpetually gets +2/+2."; + } + + private VeteranChargerEffect(final VeteranChargerEffect effect) { + super(effect); + } + + @Override + public VeteranChargerEffect copy() { + return new VeteranChargerEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + if (!controller.getHand().isEmpty()) { + Card cardFromHand = null; + if (controller.getHand().size() > 1) { + TargetCard target = new TargetCardInHand(new FilterCreatureCard()); + if (controller.choose(Outcome.AddAbility, controller.getHand(), target, source, game)) { + cardFromHand = game.getCard(target.getFirstTarget()); + } + } else { + cardFromHand = RandomUtil.randomFromCollection(controller.getHand().getCards(new FilterCreatureCard(), game)); + } + if (cardFromHand == null) { + return false; + } + game.addEffect(new BoostTargetPerpetuallyEffect(2, 2).setTargetPointer(new FixedTarget(cardFromHand, game)), source); + + } + return true; + } + return false; + } +} + diff --git a/Mage.Sets/src/mage/sets/AlchemyDominaria.java b/Mage.Sets/src/mage/sets/AlchemyDominaria.java index e465d7b2694c..69fef6544add 100644 --- a/Mage.Sets/src/mage/sets/AlchemyDominaria.java +++ b/Mage.Sets/src/mage/sets/AlchemyDominaria.java @@ -23,6 +23,7 @@ private AlchemyDominaria() { cards.add(new SetCardInfo("Ancestral Recall", 32, Rarity.MYTHIC, mage.cards.a.AncestralRecall.class)); cards.add(new SetCardInfo("Black Lotus", 35, Rarity.MYTHIC, mage.cards.b.BlackLotus.class)); + cards.add(new SetCardInfo("Bloodsprout Talisman", 21, Rarity.RARE, mage.cards.b.BloodsproutTalisman.class)); cards.add(new SetCardInfo("Marwyn's Kindred", 16, Rarity.MYTHIC, mage.cards.m.MarwynsKindred.class)); cards.add(new SetCardInfo("Mox Emerald", 36, Rarity.MYTHIC, mage.cards.m.MoxEmerald.class)); cards.add(new SetCardInfo("Mox Jet", 37, Rarity.MYTHIC, mage.cards.m.MoxJet.class)); @@ -30,6 +31,7 @@ private AlchemyDominaria() { cards.add(new SetCardInfo("Mox Ruby", 39, Rarity.MYTHIC, mage.cards.m.MoxRuby.class)); cards.add(new SetCardInfo("Mox Sapphire", 40, Rarity.MYTHIC, mage.cards.m.MoxSapphire.class)); cards.add(new SetCardInfo("Oracle of the Alpha", 4, Rarity.MYTHIC, mage.cards.o.OracleOfTheAlpha.class)); + cards.add(new SetCardInfo("Pull of the Mist Moon", 3, Rarity.UNCOMMON, mage.cards.p.PullOfTheMistMoon.class)); cards.add(new SetCardInfo("Slimefoot, Thallid Transplant", 26, Rarity.RARE, mage.cards.s.SlimefootThallidTransplant.class)); cards.add(new SetCardInfo("Time Walk", 33, Rarity.MYTHIC, mage.cards.t.TimeWalk.class)); cards.add(new SetCardInfo("Timetwister", 34, Rarity.MYTHIC, mage.cards.t.Timetwister.class)); diff --git a/Mage.Sets/src/mage/sets/AlchemyKamigawa.java b/Mage.Sets/src/mage/sets/AlchemyKamigawa.java index a040f4cdc7d5..2641d533df4b 100644 --- a/Mage.Sets/src/mage/sets/AlchemyKamigawa.java +++ b/Mage.Sets/src/mage/sets/AlchemyKamigawa.java @@ -26,6 +26,7 @@ private AlchemyKamigawa() { cards.add(new SetCardInfo("Experimental Pilot", 6, Rarity.UNCOMMON, mage.cards.e.ExperimentalPilot.class)); cards.add(new SetCardInfo("Jukai Liberator", 27, Rarity.RARE, mage.cards.j.JukaiLiberator.class)); cards.add(new SetCardInfo("Kami of Bamboo Groves", 24, Rarity.UNCOMMON, mage.cards.k.KamiOfBambooGroves.class)); + cards.add(new SetCardInfo("Painful Bond", 12, Rarity.UNCOMMON, mage.cards.p.PainfulBond.class)); cards.add(new SetCardInfo("Swarm Saboteur", 13, Rarity.RARE, mage.cards.s.SwarmSaboteur.class)); } diff --git a/Mage.Sets/src/mage/sets/JumpstartHistoricHorizons.java b/Mage.Sets/src/mage/sets/JumpstartHistoricHorizons.java index 39b54ca1d8bc..e0b5f12913d0 100644 --- a/Mage.Sets/src/mage/sets/JumpstartHistoricHorizons.java +++ b/Mage.Sets/src/mage/sets/JumpstartHistoricHorizons.java @@ -43,12 +43,13 @@ private JumpstartHistoricHorizons() { cards.add(new SetCardInfo("Arcbound Whelp", 410, Rarity.UNCOMMON, mage.cards.a.ArcboundWhelp.class)); cards.add(new SetCardInfo("Archfiend of Sorrows", 285, Rarity.UNCOMMON, mage.cards.a.ArchfiendOfSorrows.class)); cards.add(new SetCardInfo("Archmage's Charm", 166, Rarity.RARE, mage.cards.a.ArchmagesCharm.class)); - cards.add(new SetCardInfo("Assault Strobe", 786, Rarity.COMMON, mage.cards.a.AssaultStrobe.class)); cards.add(new SetCardInfo("Arcus Acolyte", 678, Rarity.UNCOMMON, mage.cards.a.ArcusAcolyte.class)); + cards.add(new SetCardInfo("Assault Strobe", 786, Rarity.COMMON, mage.cards.a.AssaultStrobe.class)); cards.add(new SetCardInfo("Asylum Visitor", 286, Rarity.RARE, mage.cards.a.AsylumVisitor.class)); cards.add(new SetCardInfo("Awaken the Bear", 542, Rarity.COMMON, mage.cards.a.AwakenTheBear.class)); cards.add(new SetCardInfo("Ayula, Queen Among Bears", 543, Rarity.RARE, mage.cards.a.AyulaQueenAmongBears.class)); cards.add(new SetCardInfo("Azra Smokeshaper", 287, Rarity.COMMON, mage.cards.a.AzraSmokeshaper.class)); + cards.add(new SetCardInfo("Baffling Defenses", 2, Rarity.COMMON, mage.cards.b.BafflingDefenses.class)); cards.add(new SetCardInfo("Bannerhide Krushok", 544, Rarity.COMMON, mage.cards.b.BannerhideKrushok.class)); cards.add(new SetCardInfo("Barbed Spike", 50, Rarity.UNCOMMON, mage.cards.b.BarbedSpike.class)); cards.add(new SetCardInfo("Batterbone", 735, Rarity.UNCOMMON, mage.cards.b.Batterbone.class)); @@ -58,6 +59,7 @@ private JumpstartHistoricHorizons() { cards.add(new SetCardInfo("Bazaar Trademage", 169, Rarity.RARE, mage.cards.b.BazaarTrademage.class)); cards.add(new SetCardInfo("Bear Cub", 546, Rarity.COMMON, mage.cards.b.BearCub.class)); cards.add(new SetCardInfo("Belligerent Sliver", 415, Rarity.UNCOMMON, mage.cards.b.BelligerentSliver.class)); + cards.add(new SetCardInfo("Benalish Partisan", 3, Rarity.RARE, mage.cards.b.BenalishPartisan.class)); cards.add(new SetCardInfo("Bestial Menace", 547, Rarity.UNCOMMON, mage.cards.b.BestialMenace.class)); cards.add(new SetCardInfo("Birthing Boughs", 736, Rarity.UNCOMMON, mage.cards.b.BirthingBoughs.class)); cards.add(new SetCardInfo("Blade Splicer", 54, Rarity.RARE, mage.cards.b.BladeSplicer.class)); @@ -100,6 +102,8 @@ private JumpstartHistoricHorizons() { cards.add(new SetCardInfo("Crocanura", 557, Rarity.COMMON, mage.cards.c.Crocanura.class)); cards.add(new SetCardInfo("Dark Salvation", 308, Rarity.RARE, mage.cards.d.DarkSalvation.class)); cards.add(new SetCardInfo("Dark Ritual", 783, Rarity.COMMON, mage.cards.d.DarkRitual.class)); + cards.add(new SetCardInfo("Davriel's Withering", 16, Rarity.COMMON, mage.cards.d.DavrielsWithering.class)); + cards.add(new SetCardInfo("Davriel, Soul Broker", 15, Rarity.MYTHIC, mage.cards.d.DavrielSoulBroker.class)); cards.add(new SetCardInfo("Death Wind", 314, Rarity.COMMON, mage.cards.d.DeathWind.class)); cards.add(new SetCardInfo("Deepwood Denizen", 558, Rarity.COMMON, mage.cards.d.DeepwoodDenizen.class)); cards.add(new SetCardInfo("Devouring Light", 67, Rarity.UNCOMMON, mage.cards.d.DevouringLight.class)); @@ -118,6 +122,7 @@ private JumpstartHistoricHorizons() { cards.add(new SetCardInfo("Duskshell Crawler", 561, Rarity.COMMON, mage.cards.d.DuskshellCrawler.class)); cards.add(new SetCardInfo("Echoing Return", 320, Rarity.COMMON, mage.cards.e.EchoingReturn.class)); cards.add(new SetCardInfo("Elusive Krasis", 688, Rarity.UNCOMMON, mage.cards.e.ElusiveKrasis.class)); + cards.add(new SetCardInfo("Ethereal Grasp", 9, Rarity.COMMON, mage.cards.e.EtherealGrasp.class)); cards.add(new SetCardInfo("Endling", 322, Rarity.RARE, mage.cards.e.Endling.class)); cards.add(new SetCardInfo("Enduring Sliver", 74, Rarity.COMMON, mage.cards.e.EnduringSliver.class)); cards.add(new SetCardInfo("Esper Sentinel", 75, Rarity.RARE, mage.cards.e.EsperSentinel.class)); @@ -143,6 +148,7 @@ private JumpstartHistoricHorizons() { cards.add(new SetCardInfo("Force Spike", 780, Rarity.COMMON, mage.cards.f.ForceSpike.class)); cards.add(new SetCardInfo("Foul Watcher", 194, Rarity.COMMON, mage.cards.f.FoulWatcher.class)); cards.add(new SetCardInfo("Foundry Street Denizen", 446, Rarity.COMMON, mage.cards.f.FoundryStreetDenizen.class)); + cards.add(new SetCardInfo("Freyalise, Skyshroud Partisan", 26, Rarity.MYTHIC, mage.cards.f.FreyaliseSkyshroudPartisan.class)); cards.add(new SetCardInfo("Funnel-Web Recluse", 573, Rarity.COMMON, mage.cards.f.FunnelWebRecluse.class)); cards.add(new SetCardInfo("Furyblade Vampire", 448, Rarity.UNCOMMON, mage.cards.f.FurybladeVampire.class)); cards.add(new SetCardInfo("Galvanic Relay", 450, Rarity.COMMON, mage.cards.g.GalvanicRelay.class)); @@ -205,19 +211,24 @@ private JumpstartHistoricHorizons() { cards.add(new SetCardInfo("Lazotep Chancellor", 702, Rarity.UNCOMMON, mage.cards.l.LazotepChancellor.class)); cards.add(new SetCardInfo("Leeching Sliver", 344, Rarity.UNCOMMON, mage.cards.l.LeechingSliver.class)); cards.add(new SetCardInfo("Legion Vanguard", 345, Rarity.UNCOMMON, mage.cards.l.LegionVanguard.class)); + cards.add(new SetCardInfo("Leonin Sanctifier", 4, Rarity.COMMON, mage.cards.l.LeoninSanctifier.class)); cards.add(new SetCardInfo("Lightning Bolt", 787, Rarity.COMMON, mage.cards.l.LightningBolt.class)); cards.add(new SetCardInfo("Lightning Spear", 484, Rarity.COMMON, mage.cards.l.LightningSpear.class)); cards.add(new SetCardInfo("Light of Hope", 777, Rarity.COMMON, mage.cards.l.LightOfHope.class)); cards.add(new SetCardInfo("Llanowar Tribe", 598, Rarity.UNCOMMON, mage.cards.l.LlanowarTribe.class)); + cards.add(new SetCardInfo("Longtusk Stalker", 27, Rarity.UNCOMMON, mage.cards.l.LongtuskStalker.class)); cards.add(new SetCardInfo("Lonis, Cryptozoologist", 703, Rarity.RARE, mage.cards.l.LonisCryptozoologist.class)); + cards.add(new SetCardInfo("Lumbering Lightshield", 5, Rarity.COMMON, mage.cards.l.LumberingLightshield.class)); cards.add(new SetCardInfo("Mad Prophet", 487, Rarity.COMMON, mage.cards.m.MadProphet.class)); cards.add(new SetCardInfo("Man-o'-War", 212, Rarity.COMMON, mage.cards.m.ManOWar.class)); + cards.add(new SetCardInfo("Managorger Phoenix", 20, Rarity.RARE, mage.cards.m.ManagorgerPhoenix.class)); cards.add(new SetCardInfo("Manaweft Sliver", 601, Rarity.UNCOMMON, mage.cards.m.ManaweftSliver.class)); cards.add(new SetCardInfo("Manic Scribe", 211, Rarity.UNCOMMON, mage.cards.m.ManicScribe.class)); cards.add(new SetCardInfo("Manor Guardian", 17, Rarity.UNCOMMON, mage.cards.m.ManorGuardian.class)); cards.add(new SetCardInfo("Markov Crusader", 354, Rarity.UNCOMMON, mage.cards.m.MarkovCrusader.class)); cards.add(new SetCardInfo("Marrow-Gnawer", 355, Rarity.RARE, mage.cards.m.MarrowGnawer.class)); cards.add(new SetCardInfo("Master of the Pearl Trident", 214, Rarity.RARE, mage.cards.m.MasterOfThePearlTrident.class)); + cards.add(new SetCardInfo("Mentor of Evos Isle", 11, Rarity.COMMON, mage.cards.m.MentorOfEvosIsle.class)); cards.add(new SetCardInfo("Mind Rake", 357, Rarity.COMMON, mage.cards.m.MindRake.class)); cards.add(new SetCardInfo("Mist-Syndicate Naga", 218, Rarity.RARE, mage.cards.m.MistSyndicateNaga.class)); cards.add(new SetCardInfo("Mob", 359, Rarity.COMMON, mage.cards.m.Mob.class)); @@ -247,6 +258,7 @@ private JumpstartHistoricHorizons() { cards.add(new SetCardInfo("Pashalik Mons", 495, Rarity.RARE, mage.cards.p.PashalikMons.class)); cards.add(new SetCardInfo("Phantasmal Form", 229, Rarity.COMMON, mage.cards.p.PhantasmalForm.class)); cards.add(new SetCardInfo("Phantom Ninja", 230, Rarity.COMMON, mage.cards.p.PhantomNinja.class)); + cards.add(new SetCardInfo("Plaguecrafter's Familiar", 18, Rarity.COMMON, mage.cards.p.PlaguecraftersFamiliar.class)); cards.add(new SetCardInfo("Ponder", 782, Rarity.COMMON, mage.cards.p.Ponder.class)); cards.add(new SetCardInfo("Pondering Mage", 231, Rarity.COMMON, mage.cards.p.PonderingMage.class)); cards.add(new SetCardInfo("Predatory Sliver", 619, Rarity.COMMON, mage.cards.p.PredatorySliver.class)); @@ -267,6 +279,7 @@ private JumpstartHistoricHorizons() { cards.add(new SetCardInfo("Recalibrate", 235, Rarity.COMMON, mage.cards.r.Recalibrate.class)); cards.add(new SetCardInfo("Reckless Charge", 500, Rarity.COMMON, mage.cards.r.RecklessCharge.class)); cards.add(new SetCardInfo("Reckless Racer", 501, Rarity.UNCOMMON, mage.cards.r.RecklessRacer.class)); + cards.add(new SetCardInfo("Reckless Ringleader", 21, Rarity.COMMON, mage.cards.r.RecklessRingleader.class)); cards.add(new SetCardInfo("Reckless Wurm", 502, Rarity.UNCOMMON, mage.cards.r.RecklessWurm.class)); cards.add(new SetCardInfo("Regal Force", 791, Rarity.RARE, mage.cards.r.RegalForce.class)); cards.add(new SetCardInfo("Renegade Tactics", 503, Rarity.COMMON, mage.cards.r.RenegadeTactics.class)); @@ -284,8 +297,10 @@ private JumpstartHistoricHorizons() { cards.add(new SetCardInfo("Sanctum Weaver", 627, Rarity.RARE, mage.cards.s.SanctumWeaver.class)); cards.add(new SetCardInfo("Sandsteppe Outcast", 121, Rarity.COMMON, mage.cards.s.SandsteppeOutcast.class)); cards.add(new SetCardInfo("Sarkhan's Scorn", 23, Rarity.COMMON, mage.cards.s.SarkhansScorn.class)); + cards.add(new SetCardInfo("Sarkhan, Wanderer to Shiv", 22, Rarity.MYTHIC, mage.cards.s.SarkhanWandererToShiv.class)); cards.add(new SetCardInfo("Savage Swipe", 630, Rarity.COMMON, mage.cards.s.SavageSwipe.class)); cards.add(new SetCardInfo("Scale Up", 631, Rarity.UNCOMMON, mage.cards.s.ScaleUp.class)); + cards.add(new SetCardInfo("Scion of Shiv", 24, Rarity.COMMON, mage.cards.s.ScionOfShiv.class)); cards.add(new SetCardInfo("Scour All Possibilities", 242, Rarity.COMMON, mage.cards.s.ScourAllPossibilities.class)); cards.add(new SetCardInfo("Scour the Desert", 122, Rarity.UNCOMMON, mage.cards.s.ScourTheDesert.class)); cards.add(new SetCardInfo("Scour the Laboratory", 243, Rarity.UNCOMMON, mage.cards.s.ScourTheLaboratory.class)); @@ -347,6 +362,7 @@ private JumpstartHistoricHorizons() { cards.add(new SetCardInfo("Tempered Sliver", 652, Rarity.UNCOMMON, mage.cards.t.TemperedSliver.class)); cards.add(new SetCardInfo("Terminal Agony", 723, Rarity.COMMON, mage.cards.t.TerminalAgony.class)); cards.add(new SetCardInfo("Territorial Kavu", 724, Rarity.RARE, mage.cards.t.TerritorialKavu.class)); + cards.add(new SetCardInfo("Teyo, Aegis Adept", 6, Rarity.MYTHIC, mage.cards.t.TeyoAegisAdept.class)); cards.add(new SetCardInfo("Thalia's Lieutenant", 148, Rarity.RARE, mage.cards.t.ThaliasLieutenant.class)); cards.add(new SetCardInfo("The First Sliver", 691, Rarity.MYTHIC, mage.cards.t.TheFirstSliver.class)); cards.add(new SetCardInfo("Thought Monitor", 261, Rarity.RARE, mage.cards.t.ThoughtMonitor.class)); @@ -373,6 +389,7 @@ private JumpstartHistoricHorizons() { cards.add(new SetCardInfo("Verdant Command", 664, Rarity.RARE, mage.cards.v.VerdantCommand.class)); cards.add(new SetCardInfo("Vermin Gorger", 401, Rarity.COMMON, mage.cards.v.VerminGorger.class)); cards.add(new SetCardInfo("Vesperlark", 156, Rarity.UNCOMMON, mage.cards.v.Vesperlark.class)); + cards.add(new SetCardInfo("Veteran Charger", 31, Rarity.COMMON, mage.cards.v.VeteranCharger.class)); cards.add(new SetCardInfo("Viashino Lashclaw", 530, Rarity.COMMON, mage.cards.v.ViashinoLashclaw.class)); cards.add(new SetCardInfo("Wall of One Thousand Cuts", 157, Rarity.COMMON, mage.cards.w.WallOfOneThousandCuts.class)); cards.add(new SetCardInfo("Warteye Witch", 404, Rarity.COMMON, mage.cards.w.WarteyeWitch.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java new file mode 100644 index 000000000000..de5363af17b6 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java @@ -0,0 +1,83 @@ +package org.mage.test.cards.digital; + +import mage.abilities.keyword.DeathtouchAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander3PlayersFFA; + +public class PerpetuallyTest extends CardTestCommander3PlayersFFA { + + private static final String familiar = "Plaguecrafter's Familiar"; + private static final String defenses = "Baffling Defenses"; + private static final String withering = "Davriel's Withering"; + + + private static final String simpleCard = "Goblin Striker"; + + /** + * Perpetually effects should be applied in any zone: + * 1. gaining 2 singleton abilities (should stay one) + * 2. changing PT boosted value + * 3. setting base power value + */ + @Test + public void testGainSingletonAbility() { + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + + addCard(Zone.HAND, playerA, familiar, 2); + addCard(Zone.HAND, playerA, simpleCard); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, familiar, true); + setChoice(playerA, simpleCard); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, familiar, true); + setChoice(playerA, simpleCard); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); + + attack(1, playerA, simpleCard, playerB); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, simpleCard, DeathtouchAbility.class, 1); + + } + + /** + * Tests perpetually gaining multiple abilities to the card in hand + */ + // TODO: доделать тест! + private static final String grasp = "Ethereal Grasp"; + + @Test + public void testGainMultipleAbilities() { + +/* addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 10); + + addCard(Zone.HAND, playerA, grasp, 2); + addCard(Zone.HAND, playerA, simpleCard); + addCard(Zone.HAND, playerA, "Murder"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, grasp, simpleCard); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, grasp, simpleCard); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Murder", simpleCard); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, simpleCard, DeathtouchAbility.class, 1);*/ + + } +} diff --git a/Mage/src/main/java/mage/abilities/common/CycleControllerTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/CycleControllerTriggeredAbility.java index ba2fe63e01ed..00564d9ba200 100644 --- a/Mage/src/main/java/mage/abilities/common/CycleControllerTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/CycleControllerTriggeredAbility.java @@ -25,7 +25,10 @@ public CycleControllerTriggeredAbility(Effect effect, boolean optional) { } public CycleControllerTriggeredAbility(Effect effect, boolean optional, boolean excludeSource) { - super(Zone.BATTLEFIELD, effect, optional); + this(Zone.BATTLEFIELD, effect, optional, excludeSource); + } + public CycleControllerTriggeredAbility(Zone zone, Effect effect, boolean optional, boolean excludeSource) { + super(zone, effect, optional); this.excludeSource = excludeSource; setTriggerPhrase("Whenever you cycle " + (excludeSource ? "another" : "a") + " card, "); } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index 35a309dff83d..ee8a46335285 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -2,6 +2,7 @@ import mage.ApprovingObject; import mage.MageObject; +import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.MageSingleton; import mage.abilities.StaticAbility; @@ -57,6 +58,8 @@ public class ContinuousEffects implements Serializable { private final Map> lastEffectsListOnLayer = new HashMap<>(); // helps to find out new effect timestamps on layers + private final Map> perpetuallyAffectedObjectsRules = new HashMap<>(); // is used to find the rules of abilities that should be colored as perpetually added + public ContinuousEffects() { applyStatus = new ApplyStatusEffect(); auraReplacementEffect = new AuraReplacementEffect(); @@ -82,6 +85,10 @@ protected ContinuousEffects(final ContinuousEffects effect) { for (Map.Entry> entry : effect.lastEffectsListOnLayer.entrySet()) { lastEffectsListOnLayer.put(entry.getKey(), entry.getValue().copy()); } + + for (Map.Entry> entry : effect.perpetuallyAffectedObjectsRules.entrySet()) { + perpetuallyAffectedObjectsRules.put(entry.getKey(), new HashSet<>(entry.getValue())); + } collectAllEffects(); order = effect.order; } @@ -1391,6 +1398,38 @@ public UUID getControllerOfSourceId(UUID sourceId) { return controllerFound; } + public Map> getPerpetuallyAffectedObjectsRules() { + return perpetuallyAffectedObjectsRules; + } + public List getPerpetuallyEffectsByCard(Card card, Game game) { + List perpetuallyEffectList = new ArrayList<>(); + for(ContinuousEffect effect : layeredEffects) { + if(effect instanceof PerpetuallyEffect) { + if(((PerpetuallyEffect) effect).affectsCard(card, game)) { + perpetuallyEffectList.add(effect); + } + } + } + return perpetuallyEffectList; + } + public void removePerpetuallyEffectsByCard(Card card, Game game) { + List perpetuallyEffectList = getPerpetuallyEffectsByCard(card, game); + for(ContinuousEffect effect : perpetuallyEffectList) { + ((PerpetuallyEffect) effect).removeTarget(card, game); + } + } + + public boolean hasPerpetuallyEffectOn(Card card, Game game) { + for(ContinuousEffect effect : layeredEffects) { + if(effect instanceof PerpetuallyEffect) { + PerpetuallyEffect perpetuallyEffect = (PerpetuallyEffect) effect; + if(perpetuallyEffect.affectsCard(card, game)) { + return true; + } + } + } + return false; + } /** * Debug only: prints out a status of the currently existing continuous effects * diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java index c8707a7289fc..1b2a8351447d 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java @@ -146,6 +146,7 @@ private boolean isInactive(T effect, Game game) { it.remove(); } break; + case Perpetually: case OneUse: if (hasOwnerLeftGame || effect.isUsed()) { it.remove(); diff --git a/Mage/src/main/java/mage/abilities/effects/PerpetuallyEffect.java b/Mage/src/main/java/mage/abilities/effects/PerpetuallyEffect.java new file mode 100644 index 000000000000..4d7968c0976e --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/PerpetuallyEffect.java @@ -0,0 +1,14 @@ +package mage.abilities.effects; + +import mage.cards.Card; +import mage.game.Game; + +import java.util.UUID; + +public interface PerpetuallyEffect extends ContinuousEffect { + boolean affectsCard(Card card, Game game); + + void removeTarget(Card card, Game game); + + void addTarget(Card card, Game game); +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/BoostSourcePerpetuallyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/BoostSourcePerpetuallyEffect.java new file mode 100644 index 000000000000..8cf9f020666e --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/BoostSourcePerpetuallyEffect.java @@ -0,0 +1,61 @@ +package mage.abilities.effects.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.BoostTargetPerpetuallyEffect; +import mage.cards.Card; +import mage.constants.Outcome; +import mage.game.Game; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; + +import java.text.DecimalFormat; +import java.text.NumberFormat; + +public class BoostSourcePerpetuallyEffect extends OneShotEffect { + + private final DynamicValue power; + private final DynamicValue toughness; + + public BoostSourcePerpetuallyEffect(int power, int toughness) { + this(StaticValue.get(power), StaticValue.get(toughness)); + } + + public BoostSourcePerpetuallyEffect(DynamicValue power, DynamicValue toughness) { + super(Outcome.BoostCreature); + NumberFormat plusMinusNF = new DecimalFormat("+#;-#"); + int addPower = ((StaticValue) power).getValue(); + int addToughness = ((StaticValue) toughness).getValue(); + staticText = "{this} perpetually gets " + plusMinusNF.format(addPower) + "/" + plusMinusNF.format(addToughness); + this.power = power; + this.toughness = toughness; + } + + protected BoostSourcePerpetuallyEffect(final BoostSourcePerpetuallyEffect effect) { + super(effect); + this.power = effect.power; + this.toughness = effect.toughness; + } + + @Override + public BoostSourcePerpetuallyEffect copy() { + return new BoostSourcePerpetuallyEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + Card card = game.getCard(source.getSourceId()); + if (!controller.getHand().isEmpty()) { + game.addEffect(new BoostTargetPerpetuallyEffect(power, toughness) + .setTargetPointer(new FixedTarget(card, game)), source); + + } + return true; + } + return false; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/CardsInYourHandPerpetuallyGainEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CardsInYourHandPerpetuallyGainEffect.java new file mode 100644 index 000000000000..e7e91bbcce05 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/CardsInYourHandPerpetuallyGainEffect.java @@ -0,0 +1,67 @@ +package mage.abilities.effects.common; + +import mage.abilities.Ability; +import mage.abilities.MageSingleton; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetPerpetuallyEffect; +import mage.cards.Card; +import mage.constants.Outcome; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.players.Player; +import mage.target.targetpointer.FixedTargets; +import mage.target.targetpointer.TargetPointer; +import mage.util.CardUtil; + +import java.util.Set; + +public class CardsInYourHandPerpetuallyGainEffect extends OneShotEffect { + + private final Ability ability; + private final FilterCard filter; + + public CardsInYourHandPerpetuallyGainEffect(Ability ability, FilterCard filter) { + super(Outcome.AddAbility); + this.staticText = filter.getMessage() + " perpetually gain "; + if(!(ability instanceof MageSingleton)){ + this.staticText += "\"" + CardUtil.getTextWithFirstCharUpperCase(ability.getRule()) + ("\""); + } + else { + ability.getRule(); + } + this.ability = ability; + this.filter = filter; + + } + + + private CardsInYourHandPerpetuallyGainEffect(final CardsInYourHandPerpetuallyGainEffect effect) { + super(effect); + this.ability = effect.ability; + this.filter = effect.filter; + } + + @Override + public CardsInYourHandPerpetuallyGainEffect copy() { + return new CardsInYourHandPerpetuallyGainEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + + if (controller != null && !controller.getHand().isEmpty()) { + Set cards = controller.getHand().getCards(filter, game); + if (cards == null) { + return false; + } + TargetPointer pointer = new FixedTargets(cards, game); + + ContinuousEffect effect = new GainAbilityTargetPerpetuallyEffect(ability).setTargetPointer(pointer); + game.addEffect(effect, source); + + } + return true; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/ChooseACardInYourHandItPerpetuallyGainsEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ChooseACardInYourHandItPerpetuallyGainsEffect.java new file mode 100644 index 000000000000..d4f33b98e99b --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/ChooseACardInYourHandItPerpetuallyGainsEffect.java @@ -0,0 +1,65 @@ +package mage.abilities.effects.common; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetPerpetuallyEffect; +import mage.cards.Card; +import mage.constants.Outcome; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInHand; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; +import mage.util.RandomUtil; + +public class ChooseACardInYourHandItPerpetuallyGainsEffect extends OneShotEffect { + + private final Ability ability; + private final FilterCard filter; + + public ChooseACardInYourHandItPerpetuallyGainsEffect(Ability ability, FilterCard filter) { + super(Outcome.AddAbility); + this.staticText = "choose " + CardUtil.addArticle(filter.getMessage()) + " in your hand. It perpetually gains " + ability.getRule(); + this.ability = ability; + this.filter = filter; + } + + protected ChooseACardInYourHandItPerpetuallyGainsEffect(final ChooseACardInYourHandItPerpetuallyGainsEffect effect) { + super(effect); + this.ability = effect.ability; + this.filter = effect.filter; + } + + @Override + public ChooseACardInYourHandItPerpetuallyGainsEffect copy() { + return new ChooseACardInYourHandItPerpetuallyGainsEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + if (!controller.getHand().isEmpty()) { + Card cardFromHand = null; + if (controller.getHand().size() > 1) { + TargetCard target = new TargetCardInHand(filter); + if (controller.choose(Outcome.AddAbility, controller.getHand(), target, source, game)) { + cardFromHand = game.getCard(target.getFirstTarget()); + } + } else { + cardFromHand = RandomUtil.randomFromCollection(controller.getHand().getCards(filter, game)); + } + if (cardFromHand == null) { + return false; + } + game.addEffect(new GainAbilityTargetPerpetuallyEffect(ability) + .setTargetPointer(new FixedTarget(cardFromHand, game)), source); + + } + return true; + } + return false; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/GainAbilitySourcePerpetuallyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/GainAbilitySourcePerpetuallyEffect.java new file mode 100644 index 000000000000..7cb180b3e654 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/GainAbilitySourcePerpetuallyEffect.java @@ -0,0 +1,50 @@ +package mage.abilities.effects.common; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetPerpetuallyEffect; +import mage.cards.Card; +import mage.constants.Outcome; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; + +public class GainAbilitySourcePerpetuallyEffect extends OneShotEffect { + + private final Ability ability; + private final FilterCard filter; + + public GainAbilitySourcePerpetuallyEffect(Ability ability, FilterCard filter) { + super(Outcome.AddAbility); + this.staticText = "it perpetually gains " + ability.getRule(); + this.ability = ability; + this.filter = filter; + } + + protected GainAbilitySourcePerpetuallyEffect(final GainAbilitySourcePerpetuallyEffect effect) { + super(effect); + this.ability = effect.ability; + this.filter = effect.filter; + } + + @Override + public GainAbilitySourcePerpetuallyEffect copy() { + return new GainAbilitySourcePerpetuallyEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + Card card = game.getCard(source.getSourceId()); + if (!controller.getHand().isEmpty()) { + game.addEffect(new GainAbilityTargetPerpetuallyEffect(ability) + .setTargetPointer(new FixedTarget(card, game)), source); + + } + return true; + } + return false; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/MeldEffect.java b/Mage/src/main/java/mage/abilities/effects/common/MeldEffect.java index 60e54932be27..0ef48f5fd903 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/MeldEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/MeldEffect.java @@ -1,7 +1,10 @@ package mage.abilities.effects.common; import mage.abilities.Ability; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.ContinuousEffects; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.PerpetuallyEffect; import mage.cards.Card; import mage.cards.Cards; import mage.cards.CardsImpl; @@ -114,9 +117,26 @@ public boolean apply(Game game, Ability source) { } MeldCard meldCard = (MeldCard) cardInfoList.get(0).createCard().copy(); meldCard.setOwnerId(controller.getId()); + + if(!game.isSimulation()) { + ContinuousEffects effects = game.getContinuousEffects(); + List sourcePerpetuallyEffects = effects.getPerpetuallyEffectsByCard(sourceCard, game); + List meldWithPerpetuallyEffects = effects.getPerpetuallyEffectsByCard(meldWithCard, game); + for(ContinuousEffect effect : sourcePerpetuallyEffects) { + ((PerpetuallyEffect) effect).addTarget(meldCard, game); + } + for(ContinuousEffect effect : meldWithPerpetuallyEffects) { + ((PerpetuallyEffect) effect).addTarget(meldCard, game); + } + effects.removePerpetuallyEffectsByCard(sourceCard, game); + effects.removePerpetuallyEffectsByCard(meldWithCard, game); + } + + meldCard.setTopHalfCard(meldWithCard, game); meldCard.setBottomHalfCard(sourceCard, game); meldCard.setMelded(true, game); + game.addMeldCard(meldCard.getId(), meldCard); game.getState().addCard(meldCard); meldCard.setZone(Zone.EXILED, game); diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/BoostTargetPerpetuallyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/BoostTargetPerpetuallyEffect.java new file mode 100644 index 000000000000..a91342324d8a --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/BoostTargetPerpetuallyEffect.java @@ -0,0 +1,234 @@ +package mage.abilities.effects.common.continuous; + +import mage.MageInt; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.PerpetuallyEffect; +import mage.cards.Card; +import mage.constants.Duration; +import mage.constants.Layer; +import mage.constants.SubLayer; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.util.CardUtil; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author karapuzz14 + */ +public class BoostTargetPerpetuallyEffect extends ContinuousEffectImpl implements PerpetuallyEffect { + + private DynamicValue power; + private DynamicValue toughness; + + private HashMap targetMap = new HashMap<>(); + + private class targetData { + boolean isUsed; + boolean wasPermanentAtInit; + int lastPowerDiff; + int lastToughnessDiff; + + public targetData() { + isUsed = false; + } + + } + + public BoostTargetPerpetuallyEffect(int power, int toughness) { + this(StaticValue.get(power), StaticValue.get(toughness)); + } + + public BoostTargetPerpetuallyEffect(DynamicValue power, DynamicValue toughness) { + super(Duration.Perpetually, Layer.PTChangingEffects_7, SubLayer.ModifyPT_7c, CardUtil.getBoostOutcome(power, toughness)); + this.power = power; + this.toughness = toughness; + } + + protected BoostTargetPerpetuallyEffect(final BoostTargetPerpetuallyEffect effect) { + super(effect); + this.power = effect.power.copy(); + this.toughness = effect.toughness.copy(); + this.targetMap = new HashMap<>(effect.targetMap); + } + + @Override + public BoostTargetPerpetuallyEffect copy() { + return new BoostTargetPerpetuallyEffect(this); + } + + @Override + public void init(Ability source, Game game) { + super.init(source, game); + + power = StaticValue.get(power.calculate(game, source, this)); + toughness = StaticValue.get(toughness.calculate(game, source, this)); + + List targetList = getTargetPointer().getTargets(game, source); + for(UUID target : targetList) { + targetMap.put(target, new targetData()); + } + targetList.stream() + .map(game::getCard) + .filter(Objects::nonNull) + .forEach(card -> { + UUID cardId = card.getId(); + MageObjectReference cardReference = new MageObjectReference(card, game); + this.affectedObjectList.add(cardReference); + + targetMap.get(cardId).wasPermanentAtInit = card.isPermanent(game) && game.getPermanent(cardId) != null; + }); + + } + + @Override + public boolean apply(Game game, Ability source) { + + int affectedTargets = 0; + for (UUID target : targetMap.keySet()) { + Card card = game.getCard(target); + if(card != null) { + + MageInt cardPower = card.getPower(); + MageInt cardToughness = card.getToughness(); + + int additionalPower = power.calculate(game, source, this); + int additionalToughness = toughness.calculate(game, source, this); + + // applying effect to the card once + if(!targetMap.get(target).isUsed) { + cardPower.increaseBoostedValue(additionalPower); + cardToughness.increaseBoostedValue(additionalToughness); + targetMap.get(target).isUsed = true; + } + + // periodically applying effect to the permanent + Permanent permanent = game.getPermanent(target); + if (permanent != null && permanent.isCreature(game)) { + if(permanent.isFaceDown(game) && !targetMap.get(target).wasPermanentAtInit) { + return false; + } + if(!permanent.isFaceDown(game) || (permanent.isFaceDown(game) && targetMap.get(target).wasPermanentAtInit)) { + permanent.addPower(additionalPower); + permanent.addToughness(additionalToughness); + if(targetMap.get(target).isUsed) { + affectedTargets++; + } + } + } + + + // calculating PT for additional info + NumberFormat plusMinusNF = new DecimalFormat("+#;-#"); + + Integer powerDiff; + Integer toughnessDiff; + + if(permanent != null && permanent.isFaceDown(game) && targetMap.get(target).wasPermanentAtInit) { + powerDiff = 0; + toughnessDiff = 0; + + List boostPerpetualEffectsOnCard = game.getContinuousEffects() + .getLayeredEffects(game).stream() + .filter(effect -> effect instanceof BoostTargetPerpetuallyEffect) + .map(effect -> (BoostTargetPerpetuallyEffect) effect) + .filter(effect -> effect.affectsCard(card, game)) + .filter(effect -> targetMap.get(target).wasPermanentAtInit) + .collect(Collectors.toList()); + + for(BoostTargetPerpetuallyEffect effect : boostPerpetualEffectsOnCard) { + powerDiff += effect.power.calculate(game, null, null); + toughnessDiff += effect.toughness.calculate(game, null, null); + + } + } + else { + powerDiff = cardPower.getValue() - cardPower.getModifiedBaseValue(); + toughnessDiff = cardToughness.getValue() - cardToughness.getModifiedBaseValue(); + + if(powerDiff == 0) { + powerDiff = targetMap.get(target).lastPowerDiff; + } else { + targetMap.get(target).lastPowerDiff = powerDiff; + } + if(toughnessDiff == 0) { + toughnessDiff = targetMap.get(target).lastToughnessDiff; + } else { + targetMap.get(target).lastToughnessDiff = toughnessDiff; + } + } + + card.addInfo("boostPTPerpetualChange", "{this}" + + " perpetually gets " + plusMinusNF.format(powerDiff) + "/" + plusMinusNF.format(toughnessDiff) + +".", game); + } + } + return affectedTargets > 0; + } + + @Override + public String getText(Mode mode) { + if (staticText != null && !staticText.isEmpty()) { + return staticText; + } + return getTargetPointer().describeTargets(mode.getTargets(), "it") + + (getTargetPointer().isPlural(mode.getTargets()) ? " each get " : " gets ") + + CardUtil.getBoostText(power, toughness, duration); + } + + @Override + public boolean affectsCard(Card card, Game game) { + for(UUID target : targetMap.keySet()) { + if(card.isPermanent(game)) { + Permanent permanent = game.getPermanent(card.getId()); + if(permanent != null && permanent.getId().equals(target)) { + return true; + } + } + if(card.getId().equals(target)) { + return true; + } + } + return false; + } + + @Override + public void removeTarget(Card card, Game game) { + for(UUID target : targetMap.keySet()) { + Card gameCard = game.getCard(card.getId()); + if(gameCard != null && gameCard.getId().equals(target)) { + if(power != null) { + gameCard.getPower().increaseBoostedValue(-1 * power.calculate(game, null, this)); + } + if(toughness != null) { + gameCard.getToughness().increaseBoostedValue(-1 * toughness.calculate(game, null, this)); + } + } + } + targetMap.keySet().removeIf(target -> card.getId().equals(target)); + if(targetMap.keySet().isEmpty()) { + this.discard(); + } + } + + @Override + public void addTarget(Card card, Game game) { + UUID cardId = card.getId(); + + targetMap.put(cardId, new targetData()); + MageObjectReference cardReference = new MageObjectReference(card, game); + this.affectedObjectList.add(cardReference); + + targetMap.get(cardId).wasPermanentAtInit = card.isPermanent(game) && game.getPermanent(cardId) != null; + + } + +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java index 6b3642f14f39..97796da01d42 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java @@ -4,6 +4,7 @@ import java.util.UUID; import mage.abilities.Ability; +import mage.abilities.effects.ContinuousEffects; import mage.abilities.effects.ReplacementEffectImpl; import mage.cards.Card; import mage.constants.Duration; @@ -139,7 +140,19 @@ public boolean replaceEvent(GameEvent event, Ability source, Game game) { Permanent permanent = zEvent.getTarget(); if (permanent != null) { Player player = game.getPlayer(permanent.getOwnerId()); + if (player != null && (forceToMove || player.chooseUse(Outcome.Benefit, "Move " + permanent.getLogName() + " to command zone instead of your " + originToZone + "?", source, game))) { + ContinuousEffects effects = game.getContinuousEffects(); + Card mainCard = permanent.getMainCard(); + + if (effects.hasPerpetuallyEffectOn(mainCard, game)) { + if (!player.chooseUse(Outcome.Benefit, "Keep all perpetual effects on " + mainCard.getLogName() + + " or remove all of them?", "You cannot pick and choose effects to keep", + "Keep all effects", "Remove all effects", null, game)) { + + effects.removePerpetuallyEffectsByCard(mainCard, game); + } + } zEvent.setToZone(Zone.COMMAND); if (!game.isSimulation()) { game.informPlayers(player.getLogName() + " has moved " + permanent.getLogName() + " to the command zone instead of their " + originToZone); diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetPerpetuallyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetPerpetuallyEffect.java new file mode 100644 index 000000000000..f452d6995ff1 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetPerpetuallyEffect.java @@ -0,0 +1,206 @@ +package mage.abilities.effects.common.continuous; + +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.MageSingleton; +import mage.abilities.Mode; +import mage.abilities.common.LinkedEffectIdStaticAbility; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.PerpetuallyEffect; +import mage.cards.Card; +import mage.constants.Duration; +import mage.constants.Layer; +import mage.constants.Outcome; +import mage.constants.SubLayer; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.util.CardUtil; + +import java.util.*; +import java.util.List; + +/** + * @author karapuzz14 + */ +public class GainAbilityTargetPerpetuallyEffect extends ContinuousEffectImpl implements PerpetuallyEffect { + + protected final Ability ability; + private HashMap morphedMap = new HashMap<>(); + + + public GainAbilityTargetPerpetuallyEffect(Ability ability) { + this(ability, null); + } + + public GainAbilityTargetPerpetuallyEffect(Ability ability, String rule) { + super(Duration.Perpetually, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, ability.getEffects().getOutcome(ability, Outcome.AddAbility)); + this.ability = copyAbility(ability); // See the method's comment, ability.copy() is not enough. + this.staticText = rule; + this.generateGainAbilityDependencies(ability, null); + } + + protected GainAbilityTargetPerpetuallyEffect(final GainAbilityTargetPerpetuallyEffect effect) { + super(effect); + this.ability = copyAbility(effect.ability); // See the method's comment, ability.copy() is not enough. + this.morphedMap = new HashMap<>(effect.morphedMap); + } + + @Override + public void init(Ability source, Game game) { + super.init(source, game); + + List targetList = getTargetPointer().getTargets(game, source); + + targetList.stream() + .map(game::getCard) + .filter(Objects::nonNull) + .forEach(card -> { + MageObjectReference cardReference = new MageObjectReference(card, game); + this.affectedObjectList.add(cardReference); + + if(card.isPermanent(game) && game.getPermanent(card.getId()) != null) { + morphedMap.put(card.getId(), true); + } + else { + morphedMap.put(card.getId(), false); + } + + if(!(ability instanceof MageSingleton && card.getAbilities().contains(ability))) { + + Map> cardRulesMap = game.getState().getContinuousEffects().getPerpetuallyAffectedObjectsRules(); + String rule = ability.getRule(); + String upperCaseRule = rule.substring(0, 1).toUpperCase() + rule.substring(1); + + if (cardRulesMap.containsKey(cardReference)) { + Set ruleSet = cardRulesMap.get(cardReference); + ruleSet.add(upperCaseRule); + } else { + Set set = new HashSet<>(); + set.add(upperCaseRule); + cardRulesMap.put(cardReference, set); + } + } + }); + + } + + @Override + public GainAbilityTargetPerpetuallyEffect copy() { + return new GainAbilityTargetPerpetuallyEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + int affectedTargets = 0; + + for (UUID targetId : morphedMap.keySet()) { + Card card = game.getCard(targetId); + + if (card != null) { + if (card.getAbilities().contains(ability)) { + continue; + } + if(card.isPermanent(game)) { + Permanent permanent = game.getPermanent(targetId); + if(permanent != null) { + if(permanent.isFaceDown(game) && !morphedMap.get(targetId)) { + return affectedTargets > 0; + } + ability.setControllerId(permanent.getControllerId()); + permanent.addAbility(ability, source.getId(), game); + affectedTargets++; + continue; + } + } + game.getState().addOtherAbility(card, ability); + affectedTargets++; + + } + } + + return affectedTargets > 0; + } + @Override + public boolean affectsCard(Card card, Game game) { + for(UUID targetId : morphedMap.keySet()) { + if(card.getId().equals(targetId)) { + return true; + } + } + return false; + } + + @Override + public void removeTarget(Card card, Game game) { + UUID cardId = card.getId(); + morphedMap.remove(cardId); + //TODO: not correct removing? + Map> cardRulesMap = game.getState().getContinuousEffects().getPerpetuallyAffectedObjectsRules(); + Set cardRefs = cardRulesMap.keySet(); + cardRefs.removeIf(ref -> ref.refersTo(cardId, game)); + if(morphedMap.isEmpty()) { + this.discard(); + } + } + + @Override + public void addTarget(Card card, Game game) { + if(card.isPermanent(game) && game.getPermanent(card.getId()) != null) { + morphedMap.put(card.getId(), true); + } + else { + morphedMap.put(card.getId(), false); + } + MageObjectReference cardReference = new MageObjectReference(card, game); + this.affectedObjectList.add(cardReference); + + if(!(ability instanceof MageSingleton && card.getAbilities().contains(ability))) { + + Map> cardRulesMap = game.getState().getContinuousEffects().getPerpetuallyAffectedObjectsRules(); + String rule = ability.getRule(); + String upperCaseRule = rule.substring(0, 1).toUpperCase() + rule.substring(1); + + if (cardRulesMap.containsKey(cardReference)) { + Set ruleSet = cardRulesMap.get(cardReference); + ruleSet.add(upperCaseRule); + } else { + Set set = new HashSet<>(); + set.add(upperCaseRule); + cardRulesMap.put(cardReference, set); + } + } + } + + /** + * Copying the ability and providing ability is needed in a few situations, + * The copy in order to have internal fields be proper to that ability in particular. + * Id must be different for the copy, for a few things like the GainAbilityTargetEffect gained + * by a clone, or in the case of an activated ability, called multiple times on the same target, + * and thus the ability should be gained multiple times. + */ + private Ability copyAbility(Ability toCopyAbility) { + Ability abilityToCopy = toCopyAbility.copy(); + abilityToCopy.newId(); + if (abilityToCopy instanceof LinkedEffectIdStaticAbility) { + ((LinkedEffectIdStaticAbility) abilityToCopy).setEffectIdManually(); + } + return abilityToCopy; + } + + @Override + public String getText(Mode mode) { + if (staticText != null && !staticText.isEmpty()) { + return staticText; + } + StringBuilder sb = new StringBuilder(getTargetPointer().describeTargets(mode.getTargets(), "it")); + sb.append("perpetually"); + sb.append(getTargetPointer().isPlural(mode.getTargets()) ? " gain " : " gains "); + if(!(ability instanceof MageSingleton)){ + sb.append("\"").append(CardUtil.getTextWithFirstCharUpperCase(ability.getRule())).append("\""); + } + else { + sb.append(CardUtil.stripReminderText(ability.getRule())); + } + return sb.toString(); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/SetBasePowerToughnessTargetPerpetuallyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/SetBasePowerToughnessTargetPerpetuallyEffect.java new file mode 100644 index 000000000000..eece2a040646 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/SetBasePowerToughnessTargetPerpetuallyEffect.java @@ -0,0 +1,176 @@ +package mage.abilities.effects.common.continuous; + +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.PerpetuallyEffect; +import mage.cards.Card; +import mage.constants.Duration; +import mage.constants.Layer; +import mage.constants.Outcome; +import mage.constants.SubLayer; +import mage.game.Game; +import mage.game.permanent.Permanent; + +import java.util.*; + +/** + * @author karapuzz14 + */ +public class SetBasePowerToughnessTargetPerpetuallyEffect extends ContinuousEffectImpl implements PerpetuallyEffect { + + private final DynamicValue power; + private final DynamicValue toughness; + + private HashMap morphedMap = new HashMap<>(); + + + public SetBasePowerToughnessTargetPerpetuallyEffect(DynamicValue power, DynamicValue toughness) { + super(Duration.Perpetually, Layer.PTChangingEffects_7, SubLayer.SetPT_7b, Outcome.Neutral); + this.power = power; + this.toughness = toughness; + } + + public SetBasePowerToughnessTargetPerpetuallyEffect(int power, int toughness) { + this(StaticValue.get(power), StaticValue.get(toughness)); + } + + protected SetBasePowerToughnessTargetPerpetuallyEffect(final SetBasePowerToughnessTargetPerpetuallyEffect effect) { + super(effect); + this.power = effect.power; + this.toughness = effect.toughness; + this.morphedMap = effect.morphedMap; + } + + @Override + public void init(Ability source, Game game) { + super.init(source, game); + + List targetList = getTargetPointer().getTargets(game, source); + + targetList.stream() + .map(game::getCard) + .filter(Objects::nonNull) + .forEach(card -> { + MageObjectReference cardReference = new MageObjectReference(card, game); + this.affectedObjectList.add(cardReference); + + if(card.isPermanent(game) && game.getPermanent(card.getId()) != null) { + morphedMap.put(card.getId(), true); + } + else { + morphedMap.put(card.getId(), false); + } + }); + + } + @Override + public SetBasePowerToughnessTargetPerpetuallyEffect copy() { + return new SetBasePowerToughnessTargetPerpetuallyEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + boolean result = false; + for (UUID targetId : morphedMap.keySet()) { + Card target = game.getCard(targetId); + if (target == null) { + continue; + } + //TODO: not finished morph? + if (power != null) { + int currentPower = power.calculate(game, source, this); + target.getPower().setModifiedBaseValue(currentPower); + target.addInfo("basePowerPerpetualChange", ""+ target.getName() + "'s base power perpetually becomes " + currentPower +".", game); + if(target.isPermanent()) { + Permanent permanent = game.getPermanent(targetId); + if (permanent != null) { + if(permanent.isFaceDown(game) && !morphedMap.get(targetId)) { + return result; + } + permanent.getPower().setModifiedBaseValue(currentPower); + } + } + } + if (toughness != null) { + int currentToughness = toughness.calculate(game, source, this); + target.getToughness().setModifiedBaseValue(currentToughness); + target.addInfo("baseToughnessPerpetualChange", ""+ target.getName() + "'s base toughness perpetually becomes " + currentToughness +".", game); + if(target.isPermanent()) { + Permanent permanent = game.getPermanent(targetId); + if (permanent != null) { + if(permanent.isFaceDown(game) && !morphedMap.get(targetId)) { + return result; + } + permanent.getToughness().setModifiedBaseValue(currentToughness); + } + } + + } + result = true; + } + return result; + } + + @Override + public String getText(Mode mode) { + if (staticText != null && !staticText.isEmpty()) { + return staticText; + } + return getTargetPointer().describeTargets(mode.getTargets(), "that creature") + + (getTargetPointer().isPlural(mode.getTargets()) ? " have" : " has") + + " base power and toughness " + power + '/' + toughness + + (duration.toString().isEmpty() ? "" : ' ' + duration.toString()); + } + + @Override + public boolean affectsCard(Card card, Game game) { + for(UUID target : morphedMap.keySet()) { + if(card.isPermanent(game)) { + Permanent permanent = game.getPermanent(card.getId()); + if(permanent != null && permanent.getId().equals(target)) { + return true; + } + } + if(card.getId().equals(target)) { + return true; + } + } + return false; + } + + @Override + public void removeTarget(Card card, Game game) { + for(UUID target : morphedMap.keySet()) { + Card gameCard = game.getCard(card.getId()); + if(gameCard != null && gameCard.getId().equals(target)) { + if(power != null) { + gameCard.getPower().resetToBaseValue(); + } + if(toughness != null) { + gameCard.getToughness().resetToBaseValue(); + } + } + } + morphedMap.keySet().removeIf(target -> card.getId().equals(target)); + if(morphedMap.isEmpty()) { + this.discard(); + } + } + + @Override + public void addTarget(Card card, Game game) { + if(card.isPermanent(game) && game.getPermanent(card.getId()) != null) { + morphedMap.put(card.getId(), true); + } + else { + morphedMap.put(card.getId(), false); + } + + MageObjectReference cardReference = new MageObjectReference(card, game); + this.affectedObjectList.add(cardReference); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/cost/SpellCostIncreaseSourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/cost/SpellCostIncreaseSourceEffect.java index 7a72c93cf06b..7663ab6488db 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/cost/SpellCostIncreaseSourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/cost/SpellCostIncreaseSourceEffect.java @@ -27,7 +27,7 @@ public SpellCostIncreaseSourceEffect(ManaCosts manaCostsToIncrease) { } public SpellCostIncreaseSourceEffect(ManaCosts manaCostsToIncrease, Condition condition) { - super(Duration.WhileOnBattlefield, Outcome.Benefit, CostModificationType.INCREASE_COST); + super(Duration.WhileOnBattlefield, Outcome.Detriment, CostModificationType.INCREASE_COST); this.amount = StaticValue.get(0); this.manaCostsToIncrease = manaCostsToIncrease; this.condition = condition; diff --git a/Mage/src/main/java/mage/constants/Duration.java b/Mage/src/main/java/mage/constants/Duration.java index dd14431a6b57..c817bd0b8409 100644 --- a/Mage/src/main/java/mage/constants/Duration.java +++ b/Mage/src/main/java/mage/constants/Duration.java @@ -6,6 +6,7 @@ public enum Duration { OneUse("", true, true), EndOfGame("for the rest of the game", false, false), + Perpetually("", false, false), WhileOnBattlefield("", false, false), WhileControlled("for as long as you control {this}", true, false), WhileOnStack("", false, true), diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 13499c2fb699..0353c826cce3 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -1412,7 +1412,12 @@ && getAllOtherAbilities(attachedTo.getId()).contains(ability))) { newAbility.newId(); } newAbility.setSourceId(attachedTo.getId()); - newAbility.setControllerId(attachedTo.getOwnerId()); + if(ability.getControllerId() != null) { + newAbility.setControllerId(ability.getControllerId()); + } + else { + newAbility.setControllerId(attachedTo.getOwnerId()); + } if (!cardState.containsKey(attachedTo.getId())) { cardState.put(attachedTo.getId(), new CardState()); } diff --git a/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerBoostCreaturesEmblem.java b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerBoostCreaturesEmblem.java new file mode 100644 index 000000000000..2ea4285fa97f --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerBoostCreaturesEmblem.java @@ -0,0 +1,36 @@ +package mage.game.command.emblems; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.constants.Duration; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.command.Emblem; + +public final class DavrielSoulBrokerBoostCreaturesEmblem extends Emblem { + + // You get an emblem with "Creatures you control get +2/+0." + public DavrielSoulBrokerBoostCreaturesEmblem() { + super("Emblem Davriel"); + + Ability ability = new SimpleStaticAbility( + Zone.COMMAND, + new BoostControlledEffect( + 2, 0, Duration.EndOfGame, + StaticFilters.FILTER_PERMANENT_CREATURES_CONTROLLED, + false + ).setText("creatures you control get +2/+0") + ); + this.getAbilities().add(ability); + } + + private DavrielSoulBrokerBoostCreaturesEmblem(final DavrielSoulBrokerBoostCreaturesEmblem card) { + super(card); + } + + @Override + public DavrielSoulBrokerBoostCreaturesEmblem copy() { + return new DavrielSoulBrokerBoostCreaturesEmblem(this); + } +} diff --git a/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerBoostOpponentCreaturesEmblem.java b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerBoostOpponentCreaturesEmblem.java new file mode 100644 index 000000000000..fd853576d6a0 --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerBoostOpponentCreaturesEmblem.java @@ -0,0 +1,36 @@ +package mage.game.command.emblems; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.constants.Duration; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.command.Emblem; + +public final class DavrielSoulBrokerBoostOpponentCreaturesEmblem extends Emblem { + + // You get an emblem with "Creatures you control get -1/-0." + public DavrielSoulBrokerBoostOpponentCreaturesEmblem() { + super("Emblem Davriel"); + + Ability ability = new SimpleStaticAbility( + Zone.COMMAND, + new BoostControlledEffect( + -1, -0, Duration.EndOfGame, + StaticFilters.FILTER_PERMANENT_CREATURE_CONTROLLED, + false + ).setText("creatures you control get -1/-0") + ); + this.getAbilities().add(ability); + } + + private DavrielSoulBrokerBoostOpponentCreaturesEmblem(final DavrielSoulBrokerBoostOpponentCreaturesEmblem card) { + super(card); + } + + @Override + public DavrielSoulBrokerBoostOpponentCreaturesEmblem copy() { + return new DavrielSoulBrokerBoostOpponentCreaturesEmblem(this); + } +} diff --git a/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerCostIncreaseEmblem.java b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerCostIncreaseEmblem.java new file mode 100644 index 000000000000..88bf3237029b --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerCostIncreaseEmblem.java @@ -0,0 +1,36 @@ +package mage.game.command.emblems; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.cost.SpellsCostIncreasingAllEffect; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.game.command.Emblem; + +public final class DavrielSoulBrokerCostIncreaseEmblem extends Emblem { + + // You get an emblem with "Spells you cast cost {B} more to cast." + public DavrielSoulBrokerCostIncreaseEmblem() { + super("Emblem Davriel"); + + Ability ability = new SimpleStaticAbility( + Zone.COMMAND, + new SpellsCostIncreasingAllEffect( + new ManaCostsImpl<>("{B}"), + new FilterCard(), + TargetController.YOU).setText("spells you cast cost {B} more to cast.") + ); + this.getAbilities().add(ability); + } + + private DavrielSoulBrokerCostIncreaseEmblem(final DavrielSoulBrokerCostIncreaseEmblem card) { + super(card); + } + + @Override + public DavrielSoulBrokerCostIncreaseEmblem copy() { + return new DavrielSoulBrokerCostIncreaseEmblem(this); + } +} diff --git a/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerCostReductionEmblem.java b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerCostReductionEmblem.java new file mode 100644 index 000000000000..6fb9210b91be --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerCostReductionEmblem.java @@ -0,0 +1,33 @@ +package mage.game.command.emblems; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.cost.SpellsCostReductionControllerEffect; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.game.command.Emblem; + +public final class DavrielSoulBrokerCostReductionEmblem extends Emblem { + + // You get an emblem with "Spells you cast cost {B} less to cast." + public DavrielSoulBrokerCostReductionEmblem() { + super("Emblem Davriel"); + + Ability ability = new SimpleStaticAbility( + Zone.COMMAND, + new SpellsCostReductionControllerEffect( + new FilterCard(), new ManaCostsImpl<>("{B}")).setText("spells you cast cost {B} less to cast.") + ); + this.getAbilities().add(ability); + } + + private DavrielSoulBrokerCostReductionEmblem(final DavrielSoulBrokerCostReductionEmblem card) { + super(card); + } + + @Override + public DavrielSoulBrokerCostReductionEmblem copy() { + return new DavrielSoulBrokerCostReductionEmblem(this); + } +} diff --git a/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerGainLifeEmblem.java b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerGainLifeEmblem.java new file mode 100644 index 000000000000..1daf073f5e66 --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerGainLifeEmblem.java @@ -0,0 +1,26 @@ +package mage.game.command.emblems; + +import mage.abilities.Ability; +import mage.abilities.common.DrawCardControllerTriggeredAbility; +import mage.abilities.effects.common.GainLifeEffect; +import mage.constants.Zone; +import mage.game.command.Emblem; + +public final class DavrielSoulBrokerGainLifeEmblem extends Emblem { + + // You get an emblem with "Whenever you draw a card, you gain 2 life." + public DavrielSoulBrokerGainLifeEmblem() { + super("Emblem Davriel"); + Ability ability = new DrawCardControllerTriggeredAbility(Zone.COMMAND, new GainLifeEffect(2), false); + this.getAbilities().add(ability); + } + + private DavrielSoulBrokerGainLifeEmblem(final DavrielSoulBrokerGainLifeEmblem card) { + super(card); + } + + @Override + public DavrielSoulBrokerGainLifeEmblem copy() { + return new DavrielSoulBrokerGainLifeEmblem(this); + } +} diff --git a/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerPlaneswalkersBuffEmblem.java b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerPlaneswalkersBuffEmblem.java new file mode 100644 index 000000000000..6ab0bdf4cdc7 --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerPlaneswalkersBuffEmblem.java @@ -0,0 +1,41 @@ +package mage.game.command.emblems; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.common.FilterControlledPlaneswalkerPermanent; +import mage.game.command.Emblem; + +public final class DavrielSoulBrokerPlaneswalkersBuffEmblem extends Emblem { + + private static final FilterControlledPlaneswalkerPermanent filter = new FilterControlledPlaneswalkerPermanent(); + + static { + filter.add(SubType.DAVRIEL.getPredicate()); + } + + // You get an emblem with "Davriel planeswalkers you control have "+2: Draw a card."" + public DavrielSoulBrokerPlaneswalkersBuffEmblem() { + super("Emblem Davriel"); + Ability ability = new SimpleStaticAbility(Zone.COMMAND, + new GainAbilityControlledEffect( + new LoyaltyAbility(new DrawCardSourceControllerEffect(1), +2), + Duration.EndOfGame, filter + ).setText("Davriel planeswalkers you control have \"+2: Draw a card.\"")); + this.getAbilities().add(ability); + } + + private DavrielSoulBrokerPlaneswalkersBuffEmblem(final DavrielSoulBrokerPlaneswalkersBuffEmblem card) { + super(card); + } + + @Override + public DavrielSoulBrokerPlaneswalkersBuffEmblem copy() { + return new DavrielSoulBrokerPlaneswalkersBuffEmblem(this); + } +} diff --git a/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerTriggeredExileEmblem.java b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerTriggeredExileEmblem.java new file mode 100644 index 000000000000..703ffb494d4d --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerTriggeredExileEmblem.java @@ -0,0 +1,30 @@ +package mage.game.command.emblems; + +import mage.abilities.Ability; +import mage.abilities.common.DrawCardControllerTriggeredAbility; +import mage.abilities.effects.common.ExileCardsFromTopOfLibraryControllerEffect; +import mage.constants.Zone; +import mage.game.command.Emblem; + +public final class DavrielSoulBrokerTriggeredExileEmblem extends Emblem { + + // You get an emblem with "Whenever you draw a card, exile the top two cards of your library." + public DavrielSoulBrokerTriggeredExileEmblem() { + super("Emblem Davriel"); + + Ability ability = new DrawCardControllerTriggeredAbility( + Zone.COMMAND, + new ExileCardsFromTopOfLibraryControllerEffect(2), + false); + this.getAbilities().add(ability); + } + + private DavrielSoulBrokerTriggeredExileEmblem(final DavrielSoulBrokerTriggeredExileEmblem card) { + super(card); + } + + @Override + public DavrielSoulBrokerTriggeredExileEmblem copy() { + return new DavrielSoulBrokerTriggeredExileEmblem(this); + } +} diff --git a/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerTriggeredLoseLifeEmblem.java b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerTriggeredLoseLifeEmblem.java new file mode 100644 index 000000000000..1eff7ba5975e --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/DavrielSoulBrokerTriggeredLoseLifeEmblem.java @@ -0,0 +1,35 @@ +package mage.game.command.emblems; + +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.command.Emblem; + +public final class DavrielSoulBrokerTriggeredLoseLifeEmblem extends Emblem { + + private static final DynamicValue xValue + = new PermanentsOnBattlefieldCount(StaticFilters.FILTER_PERMANENT_CREATURE_CONTROLLED); + + // You get an emblem with "At the beginning of your upkeep, you lose 1 life for each creature you control." + public DavrielSoulBrokerTriggeredLoseLifeEmblem() { + super("Emblem Davriel"); + + Ability ability = new BeginningOfUpkeepTriggeredAbility( + Zone.COMMAND, new LoseLifeSourceControllerEffect(xValue), TargetController.YOU, false); + this.getAbilities().add(ability); + } + + private DavrielSoulBrokerTriggeredLoseLifeEmblem(final DavrielSoulBrokerTriggeredLoseLifeEmblem card) { + super(card); + } + + @Override + public DavrielSoulBrokerTriggeredLoseLifeEmblem copy() { + return new DavrielSoulBrokerTriggeredLoseLifeEmblem(this); + } +} diff --git a/Mage/src/main/java/mage/game/command/emblems/TeyoAegisAdeptEmblem.java b/Mage/src/main/java/mage/game/command/emblems/TeyoAegisAdeptEmblem.java new file mode 100644 index 000000000000..3ec55d0023fd --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/TeyoAegisAdeptEmblem.java @@ -0,0 +1,77 @@ +package mage.game.command.emblems; + +import mage.ObjectColor; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.constants.Outcome; +import mage.constants.TargetController; +import mage.constants.Zone; + +import mage.filter.FilterCard; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.mageobject.ColorPredicate; +import mage.game.Game; +import mage.game.command.Emblem; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInYourGraveyard; + + +public final class TeyoAegisAdeptEmblem extends Emblem { + + // You get an emblem with “At the beginning of your end step, return target white creature card from your graveyard to the battlefield. You gain life equal to its toughness.” + public TeyoAegisAdeptEmblem() { + super("Emblem Teyo"); + + Ability ability = new BeginningOfEndStepTriggeredAbility(Zone.COMMAND, + new ReturnFromGraveyardToBattlefieldTargetEffect(), + TargetController.YOU, null, false); + ability.addEffect(new GainLifeEqualToToughnessEffect()); + FilterCard filter = new FilterCreatureCard("white creature card from your graveyard"); + filter.add(new ColorPredicate(ObjectColor.WHITE)); + ability.addTarget(new TargetCardInYourGraveyard(filter)); + this.getAbilities().add(ability); + } + + private TeyoAegisAdeptEmblem(final TeyoAegisAdeptEmblem card) { + super(card); + } + + @Override + public TeyoAegisAdeptEmblem copy() { + return new TeyoAegisAdeptEmblem(this); + } + +} + +class GainLifeEqualToToughnessEffect extends OneShotEffect { + + GainLifeEqualToToughnessEffect() { + super(Outcome.GainLife); + staticText = "You gain life equal to its toughness"; + } + + private GainLifeEqualToToughnessEffect(final GainLifeEqualToToughnessEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = getTargetPointer().getFirstTargetPermanentOrLKI(game, source); + if (permanent != null) { + Player player = game.getPlayer(source.getControllerId()); + if (player != null) { + player.gainLife(permanent.getToughness().getValue(), game, source); + } + } + return false; + } + + @Override + public GainLifeEqualToToughnessEffect copy() { + return new GainLifeEqualToToughnessEffect(this); + } +} diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index c16aeb3f1884..3b3f8e6f8240 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -13,6 +13,7 @@ import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.ContinuousEffects; import mage.abilities.effects.RestrictionEffect; import mage.abilities.effects.RestrictionUntapNotMoreThanEffect; import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect; @@ -4934,6 +4935,14 @@ public boolean moveCards(Set cards, Zone toZone, Ability source, break; case COMMAND: for (Card card : cards) { + ContinuousEffects effects = game.getContinuousEffects(); + if(effects.hasPerpetuallyEffectOn(card, game)) { + if (!this.chooseUse(Outcome.Benefit, "Keep all perpetual effects on " + card.getLogName() + + " or remove all of them?", "You cannot pick and choose effects to keep", + "Keep all effects", "Remove all effects", null, game)) { + effects.removePerpetuallyEffectsByCard(card, game); + } + } fromZone = game.getState().getZone(card.getId()); if (moveCardToCommandWithInfo(card, source, game, fromZone)) { successfulMovedCards.add(card); diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 49742ab086d8..915dc536db42 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1300,7 +1300,17 @@ public static List getCardRulesWithAdditionalInfo(Game game, MageObject try { List rules = rulesSource.getRules(); - if (game == null || game.getPhase() == null) { + if (game == null) { + // dynamic hints for started game only + return rules; + } + + Map> cardRulesMap = game.getState().getContinuousEffects().getPerpetuallyAffectedObjectsRules(); + if(!cardRulesMap.isEmpty()) { + handlePerpetualEffectsRules(object, cardRulesMap, rules); + } + + if(game.getPhase() == null) { // dynamic hints for started game only return rules; } @@ -1338,7 +1348,38 @@ public static List getCardRulesWithAdditionalInfo(Game game, MageObject } return RULES_ERROR_INFO; } - + private static void handlePerpetualEffectsRules(MageObject object, Map> cardRulesMap, List rules) { + for (Map.Entry> entry : cardRulesMap.entrySet()) { + MageObjectReference entryKey = entry.getKey(); + if (entryKey.getSourceId() == object.getId()) { + for (int i = 0; i < rules.size(); i++) { + String rule = rules.get(i); + if (cardRulesMap.get(entryKey).contains(rule)) { + long ruleCounts = + rules.stream() + .filter(r -> r.equals(rule)) + .count(); + if (ruleCounts != 1) { + String duplicatedRule = String.format("%s", rule + " (x" + ruleCounts + ")"); + rules.set(i, duplicatedRule); + ruleCounts--; + + while (ruleCounts > 0) { + rules.remove(rule); + ruleCounts--; + } + + cardRulesMap.get(entryKey).remove(rule); + cardRulesMap.get(entryKey).add(duplicatedRule); + } else { + String coloredRule = String.format("%s", rule); + rules.set(i, coloredRule); + } + } + } + } + } + } /** * Take control under another player, use it in inner effects like Word of Commands. Don't forget to end it in same code. * diff --git a/Mage/src/main/resources/tokens-database.txt b/Mage/src/main/resources/tokens-database.txt index 3583bd93d000..16bef9a63d00 100644 --- a/Mage/src/main/resources/tokens-database.txt +++ b/Mage/src/main/resources/tokens-database.txt @@ -136,6 +136,17 @@ |Generate|EMBLEM:M3C|Emblem Vivien|||VivienReidEmblem| |Generate|EMBLEM:ACR|Emblem Capitoline Triad|||TheCapitolineTriadEmblem| |Generate|EMBLEM:BLB|Emblem Ral|||RalCracklingWitEmblem| +|Generate|EMBLEM:J21|Emblem Davriel|||DavrielSoulBrokerBoostCreaturesEmblem| +|Generate|EMBLEM:J21|Emblem Davriel|||DavrielSoulBrokerCostReductionEmblem| +|Generate|EMBLEM:J21|Emblem Davriel|||DavrielSoulBrokerPlaneswalkersBuffEmblem| +|Generate|EMBLEM:J21|Emblem Davriel|||DavrielSoulBrokerGainLifeEmblem| +|Generate|EMBLEM:J21|Emblem Davriel|||DavrielSoulBrokerBoostOpponentCreaturesEmblem| +|Generate|EMBLEM:J21|Emblem Davriel|||DavrielSoulBrokerCostIncreaseEmblem| +|Generate|EMBLEM:J21|Emblem Davriel|||DavrielSoulBrokerTriggeredExileEmblem| +|Generate|EMBLEM:J21|Emblem Davriel|||DavrielSoulBrokerTriggeredLoseLifeEmblem| +|Generate|EMBLEM:J21|Emblem Teyo|||TeyoAegisAdeptEmblem| + + # ALL PLANES # Usage hints: From ddf8469ab288d36179afec4cb6ba87c930d91f71 Mon Sep 17 00:00:00 2001 From: karapuzz14 Date: Tue, 24 Sep 2024 00:46:50 +0300 Subject: [PATCH 02/12] Implement Fearsome Whelp --- Mage.Sets/src/mage/cards/f/FearsomeWhelp.java | 49 +++++++++++++++++++ Mage.Sets/src/mage/sets/AlchemyInnistrad.java | 1 + 2 files changed, 50 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FearsomeWhelp.java diff --git a/Mage.Sets/src/mage/cards/f/FearsomeWhelp.java b/Mage.Sets/src/mage/cards/f/FearsomeWhelp.java new file mode 100644 index 000000000000..0f81ea8317cb --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FearsomeWhelp.java @@ -0,0 +1,49 @@ + +package mage.cards.f; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.CardsInYourHandPerpetuallyGainEffect; +import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.common.FilterBySubtypeCard; + +/** + * + * @author karapuzz14 + */ +public final class FearsomeWhelp extends CardImpl { + + public FearsomeWhelp(UUID ownerId, CardSetInfo setInfo) { + super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{1}{R}"); + this.subtype.add(SubType.DRAGON); + + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // At the beginning of your end step, each Dragon card in your hand perpetually gains “This spell costs {1} less to cast.” + Ability reduceCostAbility = new SimpleStaticAbility(new SpellCostReductionSourceEffect(1)); + this.addAbility(new BeginningOfUpkeepTriggeredAbility( + new CardsInYourHandPerpetuallyGainEffect(reduceCostAbility, new FilterBySubtypeCard(SubType.DRAGON)), + TargetController.YOU, + false)); + } + + private FearsomeWhelp(final FearsomeWhelp card) { + super(card); + } + + @Override + public FearsomeWhelp copy() { + return new FearsomeWhelp(this); + } +} diff --git a/Mage.Sets/src/mage/sets/AlchemyInnistrad.java b/Mage.Sets/src/mage/sets/AlchemyInnistrad.java index ca5812d85654..cabded63bff3 100644 --- a/Mage.Sets/src/mage/sets/AlchemyInnistrad.java +++ b/Mage.Sets/src/mage/sets/AlchemyInnistrad.java @@ -25,6 +25,7 @@ private AlchemyInnistrad() { cards.add(new SetCardInfo("Cursebound Witch", 24, Rarity.UNCOMMON, mage.cards.c.CurseboundWitch.class)); cards.add(new SetCardInfo("Expedition Supplier", 6, Rarity.RARE, mage.cards.e.ExpeditionSupplier.class)); cards.add(new SetCardInfo("Faithful Disciple", 7, Rarity.UNCOMMON, mage.cards.f.FaithfulDisciple.class)); + cards.add(new SetCardInfo("Fearsome Whelp", 40, Rarity.UNCOMMON, mage.cards.f.FearsomeWhelp.class)); cards.add(new SetCardInfo("Ishkanah, Broodmother", 52, Rarity.MYTHIC, mage.cards.i.IshkanahBroodmother.class)); cards.add(new SetCardInfo("Key to the Archive", 59, Rarity.RARE, mage.cards.k.KeyToTheArchive.class)); cards.add(new SetCardInfo("Kindred Denial", 18, Rarity.UNCOMMON, mage.cards.k.KindredDenial.class)); From 32407f899d8ed663b6d65f5249d28c2e2b874072 Mon Sep 17 00:00:00 2001 From: karapuzz14 Date: Tue, 24 Sep 2024 01:51:30 +0300 Subject: [PATCH 03/12] Fix Fearsome Whelp --- Mage.Sets/src/mage/cards/f/FearsomeWhelp.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Mage.Sets/src/mage/cards/f/FearsomeWhelp.java b/Mage.Sets/src/mage/cards/f/FearsomeWhelp.java index 0f81ea8317cb..c9ac5db4b0da 100644 --- a/Mage.Sets/src/mage/cards/f/FearsomeWhelp.java +++ b/Mage.Sets/src/mage/cards/f/FearsomeWhelp.java @@ -4,7 +4,8 @@ import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; + +import mage.abilities.common.BeginningOfYourEndStepTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.CardsInYourHandPerpetuallyGainEffect; import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; @@ -32,9 +33,9 @@ public FearsomeWhelp(UUID ownerId, CardSetInfo setInfo) { // At the beginning of your end step, each Dragon card in your hand perpetually gains “This spell costs {1} less to cast.” Ability reduceCostAbility = new SimpleStaticAbility(new SpellCostReductionSourceEffect(1)); - this.addAbility(new BeginningOfUpkeepTriggeredAbility( - new CardsInYourHandPerpetuallyGainEffect(reduceCostAbility, new FilterBySubtypeCard(SubType.DRAGON)), - TargetController.YOU, + this.addAbility(new BeginningOfYourEndStepTriggeredAbility( + new CardsInYourHandPerpetuallyGainEffect(reduceCostAbility, new FilterBySubtypeCard(SubType.DRAGON)) + .setText(" each Dragon card in your hand perpetually gains \"This spell costs {1} less to cast.\""), false)); } From ddceff5449c8b4edc9482c4f9bdc0903ae7ccba3 Mon Sep 17 00:00:00 2001 From: karapuzz14 Date: Thu, 17 Oct 2024 19:14:43 +0300 Subject: [PATCH 04/12] Tests are written and adventure, split cards are fixed. --- .../test/cards/digital/PerpetuallyTest.java | 819 +++++++++++++++++- .../GainAbilityTargetPerpetuallyEffect.java | 88 +- Mage/src/main/java/mage/cards/CardImpl.java | 16 +- Mage/src/main/java/mage/game/GameImpl.java | 9 + Mage/src/main/java/mage/game/GameState.java | 13 +- Mage/src/main/java/mage/util/CardUtil.java | 32 +- 6 files changed, 907 insertions(+), 70 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java index de5363af17b6..8f5c45f4f1f0 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java @@ -1,35 +1,56 @@ package org.mage.test.cards.digital; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.CastSourceTriggeredAbility; +import mage.abilities.effects.common.UntapSourceEffect; import mage.abilities.keyword.DeathtouchAbility; +import mage.cards.Card; import mage.constants.PhaseStep; import mage.constants.Zone; -import mage.counters.CounterType; +import mage.game.Game; +import mage.game.GameException; +import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentToken; +import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestCommander3PlayersFFA; +import java.io.FileNotFoundException; +import java.util.stream.Collectors; + public class PerpetuallyTest extends CardTestCommander3PlayersFFA { private static final String familiar = "Plaguecrafter's Familiar"; + private static final String charger = "Veteran Charger"; private static final String defenses = "Baffling Defenses"; - private static final String withering = "Davriel's Withering"; - - private static final String simpleCard = "Goblin Striker"; + private static final String gigantosaurus = "Gigantosaurus"; + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + setDecknamePlayerA("CommanderDuel_UW.dck"); // Commander = Daxos of Meletis + return super.createNewGameAndPlayers(); + } /** - * Perpetually effects should be applied in any zone: + * Perpetually effects should be constantly applied in any zone * 1. gaining 2 singleton abilities (should stay one) * 2. changing PT boosted value * 3. setting base power value */ + // Gaining 2 singleton abilities (should stay one) - permanent test @Test - public void testGainSingletonAbility() { + public void testGainingSingletonAbilities() { addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); addCard(Zone.HAND, playerA, familiar, 2); + addCard(Zone.HAND, playerA, "Murder"); + addCard(Zone.HAND, playerA, "Reanimate"); addCard(Zone.HAND, playerA, simpleCard); skipInitShuffling(); @@ -39,8 +60,9 @@ public void testGainSingletonAbility() { setChoice(playerA, simpleCard); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Murder", simpleCard, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reanimate", simpleCard, true); - attack(1, playerA, simpleCard, playerB); setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); execute(); @@ -48,36 +70,795 @@ public void testGainSingletonAbility() { } + // Gaining ability - card test + @Test + public void testGainingAbilityForCard() { + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + + addCard(Zone.HAND, playerA, familiar); + addCard(Zone.HAND, playerA, "Mwonvuli Beast Tracker"); + addCard(Zone.HAND, playerA, "Sudden Setback"); + addCard(Zone.HAND, playerA, simpleCard); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, familiar, true); + setChoice(playerA, simpleCard); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sudden Setback", simpleCard, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mwonvuli Beast Tracker"); + addTarget(playerA, simpleCard); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + // verify that Beast Tracker put Goblin Striker card on the top of the library after shuffling + Card topCard = playerA.getLibrary().getFromTop(currentGame); + Assert.assertEquals(topCard.getName(), simpleCard); + } + + // Changing PT boosted value - permanent test + @Test + public void testChangingPTBoostedValue() { + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + + addCard(Zone.HAND, playerA, charger); + addCard(Zone.HAND, playerA, "Murder"); + addCard(Zone.HAND, playerA, "Reanimate"); + addCard(Zone.HAND, playerA, simpleCard); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); + setChoice(playerA, simpleCard); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Murder", simpleCard, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reanimate", simpleCard, true); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, simpleCard, 3, 3); + + } + + // Changing PT boosted value - card test + @Test + public void testChangingPTBoostedValueForCard() { + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Gigantosaurus"); + + addCard(Zone.HAND, playerA, charger); + addCard(Zone.HAND, playerA, "Corpse Explosion"); + addCard(Zone.HAND, playerA, "Murder"); + addCard(Zone.HAND, playerA, simpleCard); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); + setChoice(playerA, simpleCard); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Murder", simpleCard, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Corpse Explosion", true); + setChoice(playerA, simpleCard); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertExileCount(playerA, simpleCard, 1); + assertDamageReceived(playerA, gigantosaurus, 3); + + } + + // Setting base power value - permanent test + @Test + public void testSettingBasePTValue() { + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + + addCard(Zone.HAND, playerA, defenses); + addCard(Zone.HAND, playerA, "Murder"); + addCard(Zone.HAND, playerA, "Reanimate"); + addCard(Zone.HAND, playerA, simpleCard); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, defenses, simpleCard, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Murder", simpleCard, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reanimate", simpleCard, true); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, simpleCard, 0, 1); + assertBasePowerToughness(playerA, simpleCard, 0, 1); + + } + + // Setting PT in library - card test + @Test + public void testSettingPTForCard() { + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 10); + + addCard(Zone.BATTLEFIELD, playerA, gigantosaurus); + addCard(Zone.BATTLEFIELD, playerB, simpleCard); + + addCard(Zone.HAND, playerA, defenses); + addCard(Zone.HAND, playerA, "Sudden Setback"); + addCard(Zone.HAND, playerA, "Imperial Recruiter"); + skipInitShuffling(); + + // set Gigantosaurus base power to 0 + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, defenses, gigantosaurus, true); + + // put Gigantosaurus on the top of deck + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sudden Setback", gigantosaurus, true); + + // search for Gigantosaurus as for creature with power 2 or less + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Imperial Recruiter"); + addTarget(playerA, gigantosaurus); + + setStopAt(1, PhaseStep.END_TURN); + + execute(); + + assertHandCount(playerA, gigantosaurus, 1); + + } + /** - * Tests perpetually gaining multiple abilities to the card in hand + * Perpetually added abilities and boosts of power/toughness should be able to stack */ - // TODO: доделать тест! - private static final String grasp = "Ethereal Grasp"; + private static final String bond = "Painful Bond"; @Test public void testGainMultipleAbilities() { -/* addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); addCard(Zone.BATTLEFIELD, playerA, "Island", 10); - addCard(Zone.HAND, playerA, grasp, 2); + addCard(Zone.HAND, playerA, bond, 2); addCard(Zone.HAND, playerA, simpleCard); - addCard(Zone.HAND, playerA, "Murder"); skipInitShuffling(); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bond); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bond, true); // stack x2 + addCard(Zone.HAND, playerA, "Murder"); + addCard(Zone.HAND, playerA, "Unearth"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, grasp, simpleCard); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, grasp, simpleCard); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); // lose 2 - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Murder", simpleCard); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Murder", simpleCard, true); // lose 2 + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unearth", simpleCard, true); // lose 2 setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); execute(); - assertAbilityCount(playerA, simpleCard, DeathtouchAbility.class, 1);*/ + assertAbilityCount(playerA, simpleCard, CastSourceTriggeredAbility.class, 2); + assertLife(playerA, 34); } + + @Test + public void testGainMultiplePTBoosts() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); + + addCard(Zone.HAND, playerA, charger, 2); + addCard(Zone.HAND, playerA, simpleCard); + addCard(Zone.HAND, playerA, "Murder"); + addCard(Zone.HAND, playerA, "Unearth"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); // +2/+2 + setChoice(playerA, simpleCard); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); // +2/+2 + setChoice(playerA, simpleCard); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); // 5/5 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Murder", simpleCard, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unearth", simpleCard, true); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, simpleCard, 5, 5); + assertBasePowerToughness(playerA, simpleCard, 1, 1); + + } + + /** + * Perpetual effects behaviour is mostly defined by continuous effects rules + * 1. Perpetual effects apply in their usual layers + * 2. Perpetual effects don't change copiable values + * + */ + + // Perpetual effects apply in their usual layers + // 1) Creature perpetually gains deathtouch. 2) Humility makes creature losing all abilities, overriding perpetual effect. + @Test + public void testLoseAbilitiesAfterGaining() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + + addCard(Zone.HAND, playerA, familiar); + addCard(Zone.HAND, playerA, simpleCard); + addCard(Zone.HAND, playerA, "Humility"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, familiar, true); + setChoice(playerA, simpleCard); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Humility"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, simpleCard, DeathtouchAbility.class, 0); + + } + + // 1) Humility enters. 2) Creature enters and loses abilities. 3) Creature gains abilities from Ethereal Grasp. + // 4) Ethereal Grasp abilities are present because of overriding Humility effect - timestamp order. + @Test + public void testGainAbilityAfterLosing() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + addCard(Zone.HAND, playerA, simpleCard); + addCard(Zone.HAND, playerA, "Ethereal Grasp"); + addCard(Zone.HAND, playerA, "Humility"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Humility", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", simpleCard, true); + + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, simpleCard, SimpleActivatedAbility.class, 1); + + } + + // Perpetual effects don't change copiable values. + @Test + public void testDontChangeCopiableValues() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + + addCard(Zone.HAND, playerA, charger); + addCard(Zone.HAND, playerA, simpleCard); + addCard(Zone.HAND, playerA, "Ethereal Grasp"); + addCard(Zone.HAND, playerA, "Cackling Counterpart"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); + setChoice(playerA, simpleCard); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", simpleCard, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cackling Counterpart", simpleCard, true); + + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + Permanent token = currentGame.getBattlefield().getAllPermanents().stream() + .filter(permanent -> permanent instanceof PermanentToken) + .collect(Collectors.toList()).get(0); + + Assert.assertEquals(1, token.getPower().getValue()); + Assert.assertEquals(1, token.getToughness().getValue()); + Assert.assertFalse(token.hasAbility( + new SimpleActivatedAbility(Zone.BATTLEFIELD, new UntapSourceEffect(), new GenericManaCost(8)), currentGame)); + + } + + /** + * Perpetual effects tests for specific card types + * + */ + + // Adventure cards test. + // Perpetual effects affect both sides of adventure cards. + + @Test + public void testAdventureBothSidesAffected() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); + + + addCard(Zone.HAND, playerA, bond); + addCard(Zone.HAND, playerA, "Mosswood Dreadknight"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bond, true); + + // lose 1 life from Dread Whispers and 1 life from Painful Bond + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dread Whispers", true); + + // lose 1 life from Painful Bond + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mosswood Dreadknight", true); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerA, 37); + + } + + // Aftermath cards test. + // Perpetual effects affect both sides of aftermath cards. + + @Test + public void testAftermathBothSidesAffected() { + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerA, "Bloodsprout Talisman"); + + addCard(Zone.HAND, playerA, "Driven // Despair"); + + skipInitShuffling(); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Pay 1 life: Choose a nonland card in your hand. It perpetually gains this spell costs {1} less to cast."); + setChoice(playerA, "Driven // Despair"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Driven", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Despair", true); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertExileCount(playerA, "Driven // Despair", 1); + } + + // Craft cards test. + // Only perpetual effects that affect craft card are applied to the crafted card. + @Test + public void testCraftCard() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + addCard(Zone.HAND, playerA, simpleCard); + addCard(Zone.HAND, playerA, "Tithing Blade"); + addCard(Zone.HAND, playerA, "Pull of the Mist Moon"); + addCard(Zone.HAND, playerA, "Ethereal Grasp"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, simpleCard, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", simpleCard, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pull of the Mist Moon"); + setChoice(playerA, true); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + setChoice(playerA, "Tithing Blade"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tithing Blade",true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Craft with creature {4}{B}"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + setStopAt(2, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, "Consuming Sepulcher", SimpleActivatedAbility.class, 0); + assertAbilityCount(playerA, "Consuming Sepulcher", EntersBattlefieldTriggeredAbility.class, 1); + + } + // Transform cards test. + // Perpetual effects affect both sides of transform cards. + @Test + public void testTransformFaceAffected() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + + addCard(Zone.HAND, playerA, charger); + addCard(Zone.HAND, playerA, "Weaver of Blossoms"); + addCard(Zone.HAND, playerA, "Ethereal Grasp"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); + setChoice(playerA, "Weaver of Blossoms"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Weaver of Blossoms", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", "Weaver of Blossoms", true); + + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, "Weaver of Blossoms", SimpleActivatedAbility.class, 1); + assertPowerToughness(playerA, "Weaver of Blossoms", 4, 5); + + } + + @Test + public void testTransformBackAffected() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + + addCard(Zone.HAND, playerA, charger); + addCard(Zone.HAND, playerA, "Weaver of Blossoms"); + addCard(Zone.HAND, playerA, "Ethereal Grasp"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); + setChoice(playerA, "Weaver of Blossoms"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Weaver of Blossoms", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", "Weaver of Blossoms", true); + + // there's night on the 3rd turn + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, "Blossom-Clad Werewolf", SimpleActivatedAbility.class, 1); // transformed version + assertPowerToughness(playerA, "Blossom-Clad Werewolf", 5, 6); + + } + + // Meld cards test. + // 1. Melded card becomes the target of all perpetual effects that affected its meld pair. + // 2. When melded card goes into any zone except of battlefield, it becomes two cards. These cards have no perpetual effects. + + @Test + public void testMeldedCardCombinesPerpetualEffects() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 15); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + + addCard(Zone.HAND, playerA, charger); + addCard(Zone.HAND, playerA, "Ethereal Grasp"); + addCard(Zone.HAND, playerA, "Mishra, Claimed by Gix"); + addCard(Zone.HAND, playerA, "Phyrexian Dragon Engine"); + addCard(Zone.HAND, playerA, "Burst of Speed"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); + setChoice(playerA, "Mishra, Claimed by Gix"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mishra, Claimed by Gix", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Phyrexian Dragon Engine", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", "Phyrexian Dragon Engine", true); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{8}: Untap {this}."); + + + // meld cards into Mishra, Lost to Phyrexia + attack(4, playerA, "Phyrexian Dragon Engine", playerB); + attack(4, playerA, "Mishra, Claimed by Gix", playerB); + setChoice(playerA, "Phyrexian Dragon Engine"); + setModeChoice(playerA, "4"); + setModeChoice(playerA, "5"); + setModeChoice(playerA, "6"); + waitStackResolved(4, PhaseStep.DECLARE_ATTACKERS); + + setStopAt(4, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, "Mishra, Lost to Phyrexia", SimpleActivatedAbility.class, 1); + assertPowerToughness(playerA, "Mishra, Lost to Phyrexia", 11, 11); + } + + // Disturb cards test. + // Perpetual effects affect both sides of disturb cards. + @Test + public void testDisturbKeepsEffects() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + + addCard(Zone.HAND, playerA, charger); + addCard(Zone.HAND, playerA, "Murder"); + addCard(Zone.HAND, playerA, "Lunarch Veteran"); + addCard(Zone.HAND, playerA, "Ethereal Grasp"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); + setChoice(playerA, "Lunarch Veteran"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lunarch Veteran", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", "Lunarch Veteran", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Murder", "Lunarch Veteran", true); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Luminous Phantom using Disturb"); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, "Luminous Phantom", SimpleActivatedAbility.class, 1); + assertPowerToughness(playerA, "Luminous Phantom", 3, 3); + + } + + // Morph (disguise, etc.) card tests. + // Perpetual effects applied to the card in any zone are hidden when card is cast as morph. + // Perpetual effects applied to the morphed permanent affect both sides. + + // Test hidden ability not to show and disguise permanent ability to show + @Test + public void testMorphGainAbilityMorph() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + addCard(Zone.HAND, playerA, "Unyielding Gatekeeper"); + addCard(Zone.HAND, playerA, familiar); + addCard(Zone.HAND, playerA, "Ethereal Grasp"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, familiar, true); + setChoice(playerA, "Unyielding Gatekeeper"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unyielding Gatekeeper using Disguise", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", "", true); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, "", SimpleActivatedAbility.class, 1); + assertAbilityCount(playerA, "", DeathtouchAbility.class, 0); + } + + // Test hidden ability and disguise permanent ability to show after turning face up + @Test + public void testMorphGainAbilityCard() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + addCard(Zone.HAND, playerA, "Unyielding Gatekeeper"); + addCard(Zone.HAND, playerA, familiar); + addCard(Zone.HAND, playerA, "Ethereal Grasp"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, familiar, true); + setChoice(playerA, "Unyielding Gatekeeper"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unyielding Gatekeeper using Disguise", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", "", true); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}: Turn this face-down permanent face up."); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, "Unyielding Gatekeeper", SimpleActivatedAbility.class, 1); + assertAbilityCount(playerA, "Unyielding Gatekeeper", DeathtouchAbility.class, 1); + } + + // Test hidden boost not to show + @Test + public void testMorphBoostPTMorph() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + addCard(Zone.HAND, playerA, "Unyielding Gatekeeper"); + addCard(Zone.HAND, playerA, charger); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); + setChoice(playerA, "Unyielding Gatekeeper"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unyielding Gatekeeper using Disguise", true); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, "", 2, 2); + } + + // Test hidden boost to show + @Test + public void testMorphBoostPTCard() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + addCard(Zone.HAND, playerA, "Unyielding Gatekeeper"); + addCard(Zone.HAND, playerA, charger); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); + setChoice(playerA, "Unyielding Gatekeeper"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unyielding Gatekeeper using Disguise", true); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}: Turn this face-down permanent face up."); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, "Unyielding Gatekeeper", 5, 4); + + } + + /** + * Perpetual effects additional rules for Brawl (Freeform Commander here) + * + */ + + // Perpetual effects on commander can be kept in command zone + @Test + public void testCommanderKeepsPerpetualEffects() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 10); + + addCard(Zone.HAND, playerA, "Unsummon"); + addCard(Zone.HAND, playerA, "Murder"); + addCard(Zone.HAND, playerA, familiar); + addCard(Zone.HAND, playerA, charger); + + skipInitShuffling(); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Daxos of Meletis", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unsummon", "Daxos of Meletis"); + setChoice(playerA, false); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, familiar, true); + setChoice(playerA, "Daxos of Meletis"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); + setChoice(playerA, "Daxos of Meletis"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Daxos of Meletis", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Murder", "Daxos of Meletis", true); + setChoice(playerA, true); + setChoice(playerA, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Daxos of Meletis", true); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, "Daxos of Meletis", DeathtouchAbility.class, 1); + assertPowerToughness(playerA, "Daxos of Meletis", 4, 4); + + } + + // Perpetual effects on commander can be cleared in command zone + @Test + public void testCommanderClearsPerpetualEffects() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 10); + + addCard(Zone.HAND, playerA, "Unsummon"); + addCard(Zone.HAND, playerA, "Murder"); + addCard(Zone.HAND, playerA, familiar); + addCard(Zone.HAND, playerA, charger); + + skipInitShuffling(); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Daxos of Meletis", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unsummon", "Daxos of Meletis"); + setChoice(playerA, false); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, familiar, true); + setChoice(playerA, "Daxos of Meletis"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); + setChoice(playerA, "Daxos of Meletis"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Daxos of Meletis", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Murder", "Daxos of Meletis", true); + setChoice(playerA, true); + setChoice(playerA, false); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Daxos of Meletis", true); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, "Daxos of Meletis", DeathtouchAbility.class, 0); + assertPowerToughness(playerA, "Daxos of Meletis", 2, 2); + + } + } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetPerpetuallyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetPerpetuallyEffect.java index f452d6995ff1..5ad2d2bed0a4 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetPerpetuallyEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetPerpetuallyEffect.java @@ -1,5 +1,6 @@ package mage.abilities.effects.common.continuous; +import mage.MageObjectImpl; import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.MageSingleton; @@ -7,11 +8,8 @@ import mage.abilities.common.LinkedEffectIdStaticAbility; import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.PerpetuallyEffect; -import mage.cards.Card; -import mage.constants.Duration; -import mage.constants.Layer; -import mage.constants.Outcome; -import mage.constants.SubLayer; +import mage.cards.*; +import mage.constants.*; import mage.game.Game; import mage.game.permanent.Permanent; import mage.util.CardUtil; @@ -55,30 +53,17 @@ public void init(Ability source, Game game) { .map(game::getCard) .filter(Objects::nonNull) .forEach(card -> { - MageObjectReference cardReference = new MageObjectReference(card, game); - this.affectedObjectList.add(cardReference); + if (!card.getSpellAbility().getSpellAbilityType().equals(SpellAbilityType.SPLIT_FUSED)) { + MageObjectReference cardReference = new MageObjectReference(card, game); + this.affectedObjectList.add(cardReference); - if(card.isPermanent(game) && game.getPermanent(card.getId()) != null) { - morphedMap.put(card.getId(), true); - } - else { - morphedMap.put(card.getId(), false); - } - - if(!(ability instanceof MageSingleton && card.getAbilities().contains(ability))) { - - Map> cardRulesMap = game.getState().getContinuousEffects().getPerpetuallyAffectedObjectsRules(); - String rule = ability.getRule(); - String upperCaseRule = rule.substring(0, 1).toUpperCase() + rule.substring(1); - - if (cardRulesMap.containsKey(cardReference)) { - Set ruleSet = cardRulesMap.get(cardReference); - ruleSet.add(upperCaseRule); - } else { - Set set = new HashSet<>(); - set.add(upperCaseRule); - cardRulesMap.put(cardReference, set); + if(card.isPermanent(game) && game.getPermanent(card.getId()) != null) { + morphedMap.put(card.getId(), true); + } + else { + morphedMap.put(card.getId(), false); } + addTarget(card, game); } }); @@ -112,7 +97,37 @@ public boolean apply(Game game, Ability source) { continue; } } - game.getState().addOtherAbility(card, ability); + if(card instanceof SplitCard) { + SplitCardHalf left = ((SplitCard) card).getLeftHalfCard(); + Ability leftAbility = copyAbility(ability); + leftAbility.setControllerId(source.getControllerId()); + leftAbility.setSourceId(left.getId()); + addTarget(left, game); + game.getState().addOtherAbility(left, leftAbility); + + SplitCardHalf right = ((SplitCard) card).getRightHalfCard(); + Ability rightAbility = copyAbility(ability); + rightAbility.setControllerId(source.getControllerId()); + rightAbility.setSourceId(right.getId()); + addTarget(right, game); + game.getState().addOtherAbility(right, rightAbility); + } + else { + game.getState().addOtherAbility(card, ability); + } + if(card instanceof AdventureCard) { + MageObjectImpl spellCardObj = (MageObjectImpl) ((AdventureCard) card).getSpellCard(); + UUID adventureId = spellCardObj.getId(); + if (adventureId != null) { + Card spellCard = (Card) spellCardObj; + Ability newAbility = copyAbility(ability); + newAbility.setSourceId(adventureId); + newAbility.setControllerId(source.getControllerId()); + + addTarget(spellCard, game); + game.getState().addOtherAbility(spellCard, newAbility); + } + } affectedTargets++; } @@ -145,15 +160,18 @@ public void removeTarget(Card card, Game game) { @Override public void addTarget(Card card, Game game) { - if(card.isPermanent(game) && game.getPermanent(card.getId()) != null) { - morphedMap.put(card.getId(), true); - } - else { - morphedMap.put(card.getId(), false); + if (card.getSpellAbility().getSpellAbilityType().equals(SpellAbilityType.SPLIT_FUSED)) { + return; } MageObjectReference cardReference = new MageObjectReference(card, game); - this.affectedObjectList.add(cardReference); - + if(!(card instanceof AdventureCardSpell || card instanceof SplitCardHalf)) { + if (card.isPermanent(game) && game.getPermanent(card.getId()) != null) { + morphedMap.put(card.getId(), true); + } else { + morphedMap.put(card.getId(), false); + } + this.affectedObjectList.add(cardReference); + } if(!(ability instanceof MageSingleton && card.getAbilities().contains(ability))) { Map> cardRulesMap = game.getState().getContinuousEffects().getPerpetuallyAffectedObjectsRules(); diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index 651baee06137..6db2dee5252d 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -262,8 +262,13 @@ public Abilities getAbilities(Game game) { if (game == null) { return abilities; // deck editor with empty game } - - CardState cardState = game.getState().getCardState(this.getId()); + CardState cardState; + if(this instanceof AdventureCardSpell || this instanceof SplitCardHalf) { + MageObjectImpl thisObj = this; + cardState = game.getState().getCardState(thisObj.getId()); + } else { + cardState = game.getState().getCardState(this.getId()); + } if (cardState == null) { return abilities; } @@ -429,7 +434,12 @@ public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID control ZoneChangeEvent event = new ZoneChangeEvent(mainCard.getId(), ability, controllerId, fromZone, Zone.STACK); Spell spell = new Spell(this, ability.getSpellAbilityToResolve(game), controllerId, event.getFromZone(), game); ZoneChangeInfo.Stack info = new ZoneChangeInfo.Stack(event, spell); - return ZonesHandler.cast(info, ability, game); + + boolean result = ZonesHandler.cast(info, ability, game); + if((this instanceof AdventureCardSpell || this instanceof SplitCardHalf) && result) { + this.setZone(Zone.STACK, game); + } + return result; } @Override diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index ba5d909dcf11..d662359df755 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -2397,6 +2397,15 @@ protected boolean checkStateBasedActions() { if (player.chooseUse(Outcome.Benefit, "Move " + card.getLogName() + " to the command zone or leave it in current zone " + currentZoneInfo + "?", "You can only make this choice once per object", "Move to command", "Leave in current zone " + currentZoneInfo, null, this)) { + ContinuousEffects effects = getContinuousEffects(); + if (effects.hasPerpetuallyEffectOn(card, this)) { + if (!player.chooseUse(Outcome.Benefit, "Keep all perpetual effects on " + card.getLogName() + + " or remove all of them?", "You cannot pick and choose effects to keep", + "Keep all effects", "Remove all effects", null, this)) { + + effects.removePerpetuallyEffectsByCard(card, this); + } + } toMove.add(card); } else { state.setCommanderShouldStay(card, this); diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 0353c826cce3..f8a4070fdd59 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -1411,18 +1411,21 @@ && getAllOtherAbilities(attachedTo.getId()).contains(ability))) { newAbility = ability.copy(); newAbility.newId(); } - newAbility.setSourceId(attachedTo.getId()); + if(!(attachedTo instanceof AdventureCardSpell || attachedTo instanceof SplitCardHalf)) { + newAbility.setSourceId(attachedTo.getId()); + } if(ability.getControllerId() != null) { newAbility.setControllerId(ability.getControllerId()); } else { newAbility.setControllerId(attachedTo.getOwnerId()); } - if (!cardState.containsKey(attachedTo.getId())) { - cardState.put(attachedTo.getId(), new CardState()); + UUID attachedToCardId = attachedTo.getId(); + if (!cardState.containsKey(attachedToCardId)) { + cardState.put(attachedToCardId, new CardState()); } - cardState.get(attachedTo.getId()).addAbility(newAbility); - addAbility(newAbility, attachedTo.getId(), attachedTo); + cardState.get(attachedToCardId).addAbility(newAbility); + addAbility(newAbility, attachedToCardId, attachedTo); } private void checkWrongDynamicAbilityUsage(Card attachedTo, Ability ability) { diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 915dc536db42..c4decce053c4 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1307,7 +1307,7 @@ public static List getCardRulesWithAdditionalInfo(Game game, MageObject Map> cardRulesMap = game.getState().getContinuousEffects().getPerpetuallyAffectedObjectsRules(); if(!cardRulesMap.isEmpty()) { - handlePerpetualEffectsRules(object, cardRulesMap, rules); + handlePerpetualEffectsRules(object, cardRulesMap, rules, game); } if(game.getPhase() == null) { @@ -1348,25 +1348,27 @@ public static List getCardRulesWithAdditionalInfo(Game game, MageObject } return RULES_ERROR_INFO; } - private static void handlePerpetualEffectsRules(MageObject object, Map> cardRulesMap, List rules) { + private static void handlePerpetualEffectsRules(MageObject object, Map> cardRulesMap, List rules, Game game) { for (Map.Entry> entry : cardRulesMap.entrySet()) { MageObjectReference entryKey = entry.getKey(); if (entryKey.getSourceId() == object.getId()) { for (int i = 0; i < rules.size(); i++) { String rule = rules.get(i); - if (cardRulesMap.get(entryKey).contains(rule)) { + if (cardRulesMap.get(entryKey).stream().anyMatch(s -> s.contains(rule))) { long ruleCounts = rules.stream() .filter(r -> r.equals(rule)) .count(); - if (ruleCounts != 1) { - String duplicatedRule = String.format("%s", rule + " (x" + ruleCounts + ")"); + // coloring perpetual text + long j = ruleCounts; + if (j != 1) { + String duplicatedRule = String.format("%s", rule + " (x" + j + ")"); rules.set(i, duplicatedRule); - ruleCounts--; + j--; - while (ruleCounts > 0) { + while (j > 0) { rules.remove(rule); - ruleCounts--; + j--; } cardRulesMap.get(entryKey).remove(rule); @@ -1375,6 +1377,20 @@ private static void handlePerpetualEffectsRules(MageObject object, Map%s", rule); rules.set(i, coloredRule); } + + // clearing main adventure card perpetual text + if(object instanceof AdventureCard) { + long sharedRulesCounts = ((AdventureCard) object).getSharedAbilities(game).stream() + .filter(a -> a.getRule().equals(rule)) + .count(); + if(ruleCounts > sharedRulesCounts) { + rules.removeIf(r -> r.contains(rule)); + } + } + // clearing main split card perpetual text + if(object instanceof SplitCard) { + rules.removeIf(r -> r.contains(rule)); + } } } } From cec103631515333b8ab6115aad7935c70738877d Mon Sep 17 00:00:00 2001 From: karapuzz14 Date: Thu, 17 Oct 2024 22:05:06 +0300 Subject: [PATCH 05/12] Perpetually PT boost colorizing --- .../mage/client/game/BattlefieldPanel.java | 3 ++ .../mage/card/arcane/CardPanelAttributes.java | 7 +++- .../card/arcane/CardPanelRenderModeImage.java | 10 ++--- .../card/arcane/CardPanelRenderModeMTGO.java | 2 +- .../mage/card/arcane/CardRendererUtils.java | 10 +++-- .../mage/card/arcane/ModernCardRenderer.java | 4 +- .../card/arcane/ModernSplitCardRenderer.java | 2 +- .../src/main/java/mage/view/CardView.java | 42 +++++++++++++++++++ .../BoostTargetPerpetuallyEffect.java | 11 +++++ ...PowerToughnessTargetPerpetuallyEffect.java | 8 ++++ 10 files changed, 86 insertions(+), 13 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/game/BattlefieldPanel.java b/Mage.Client/src/main/java/mage/client/game/BattlefieldPanel.java index 7714d3a66daa..480bee5ffab9 100644 --- a/Mage.Client/src/main/java/mage/client/game/BattlefieldPanel.java +++ b/Mage.Client/src/main/java/mage/client/game/BattlefieldPanel.java @@ -153,6 +153,9 @@ public void update(Map battlefield) { MagePermanent oldMagePermanent = oldFound == null ? null : (MagePermanent) oldFound.getMainPanel(); // Check if there was a change in the power or toughness of the permanent + if(permanent.isPowerPerpetuallyAffected() || permanent.isToughnessPerpetuallyAffected()) { + changed = true; + } int permanentPower = 0; int permanentToughness = 0; int oldMagePermanentPower = 0; diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelAttributes.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelAttributes.java index 0904a528b871..162ed5f88e49 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelAttributes.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelAttributes.java @@ -11,12 +11,17 @@ public class CardPanelAttributes { public final boolean isSelected; public final boolean isChoosable; public final boolean isTransformed; + public final boolean isPowerPerpetuallyAffected; + public final boolean isToughnessPerpetuallyAffected; - public CardPanelAttributes(int cardWidth, int cardHeight, boolean isChoosable, boolean isSelected, boolean isTransformed) { + public CardPanelAttributes(int cardWidth, int cardHeight, boolean isChoosable, boolean isSelected, boolean isTransformed, + boolean isPowerPerpetuallyAffected, boolean isToughnessPerpetuallyAffected) { this.cardWidth = cardWidth; this.cardHeight = cardHeight; this.isChoosable = isChoosable; this.isSelected = isSelected; this.isTransformed = isTransformed; + this.isPowerPerpetuallyAffected = isPowerPerpetuallyAffected; + this.isToughnessPerpetuallyAffected = isToughnessPerpetuallyAffected; } } diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelRenderModeImage.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelRenderModeImage.java index e271704536d5..342e9d75eea8 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelRenderModeImage.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelRenderModeImage.java @@ -456,9 +456,9 @@ public void doLayout() { MageInt currentPower = cardView.getOriginalPower(); MageInt currentToughness = cardView.getOriginalToughness(); - prepareGlowFont(ptText1, Math.max(CARD_PT_FONT_MIN_SIZE, mainFontSize), currentPower, false); - prepareGlowFont(ptText2, Math.max(CARD_PT_FONT_MIN_SIZE, mainFontSize), null, false); - prepareGlowFont(ptText3, Math.max(CARD_PT_FONT_MIN_SIZE, mainFontSize), currentToughness, CardRendererUtils.isCardWithDamage(cardView)); + prepareGlowFont(ptText1, Math.max(CARD_PT_FONT_MIN_SIZE, mainFontSize), currentPower, false, getCard().isPowerPerpetuallyAffected()); + prepareGlowFont(ptText2, Math.max(CARD_PT_FONT_MIN_SIZE, mainFontSize), null, false, false); + prepareGlowFont(ptText3, Math.max(CARD_PT_FONT_MIN_SIZE, mainFontSize), currentToughness, CardRendererUtils.isCardWithDamage(cardView), getCard().isToughnessPerpetuallyAffected()); // right bottom corner with margin (sizes from any sample card) int ptMarginRight = Math.round(64f / 672f * cardWidth); @@ -677,9 +677,9 @@ private int getManaWidth(String manaCost, int symbolMarginX) { return width; } - private void prepareGlowFont(GlowText label, int fontSize, MageInt value, boolean drawAsDamaged) { + private void prepareGlowFont(GlowText label, int fontSize, MageInt value, boolean drawAsDamaged, boolean isPerpetuallyAffected) { label.setFont(getFont().deriveFont(Font.BOLD, fontSize)); - label.setForeground(CardRendererUtils.getCardTextColor(value, drawAsDamaged, titleText.getForeground(), true)); + label.setForeground(CardRendererUtils.getCardTextColor(value, drawAsDamaged, titleText.getForeground(), true, isPerpetuallyAffected)); Dimension ptSize = label.getPreferredSize(); label.setSize(ptSize.width, ptSize.height); } diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelRenderModeMTGO.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelRenderModeMTGO.java index db9c89f93a2a..47dd5909cddb 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelRenderModeMTGO.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelRenderModeMTGO.java @@ -316,7 +316,7 @@ public void updateArtImage() { } private CardPanelAttributes getAttributes() { - return new CardPanelAttributes(getCardWidth(), getCardHeight(), isChoosable(), isSelected(), isTransformed()); + return new CardPanelAttributes(getCardWidth(), getCardHeight(), isChoosable(), isSelected(), isTransformed(), getCard().isPowerPerpetuallyAffected(), getCard().isToughnessPerpetuallyAffected()); } /** diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardRendererUtils.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardRendererUtils.java index c2c07feb0086..7eeb71dc64c2 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardRendererUtils.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardRendererUtils.java @@ -31,6 +31,7 @@ public final class CardRendererUtils { private static final Color CARD_TEXT_COLOR_GOOD_DARK = new Color(52, 135, 88); private static final Color CARD_TEXT_COLOR_BAD_LIGHT = new Color(234, 153, 153); private static final Color CARD_TEXT_COLOR_BAD_DARK = new Color(200, 33, 33); + private static final Color CARD_TEXT_COLOR_PERPETUAL = new Color(204, 79, 254); /** * Convert an abstract image, whose underlying implementation may or may not @@ -239,15 +240,18 @@ public static boolean isCardWithDamage(CardView cardView) { return haveDamage; } - public static Color getCardTextColor(MageInt value, boolean drawAsDamaged, Color defaultColor, boolean textLight) { + public static Color getCardTextColor(MageInt value, boolean drawAsDamaged, Color defaultColor, boolean textLight, boolean isPerpetuallyAffected) { if (drawAsDamaged) { return textLight ? CARD_TEXT_COLOR_BAD_LIGHT : CARD_TEXT_COLOR_BAD_DARK; } - - // boost colorizing if (value != null) { int currentValue = value.getValue(); int baseValue = value.getModifiedBaseValue(); + // perpetual boost colorizing + if(isPerpetuallyAffected) { + return CARD_TEXT_COLOR_PERPETUAL; + } + // boost colorizing if (currentValue < baseValue) { return textLight ? CARD_TEXT_COLOR_BAD_LIGHT : CARD_TEXT_COLOR_BAD_DARK; } else if (currentValue > baseValue) { diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java b/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java index eb2643061dc6..f9277fb8628c 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java @@ -1043,13 +1043,13 @@ protected void drawBottomRight(Graphics2D g, CardPanelAttributes attribs, Paint int ptPosStart2 = ptPosStart1 + ptTextWidth1 + ptDeviderSpace; int ptPosStart3 = ptPosStart2 + ptTextWidth2 + ptDeviderSpace; // p - g.setColor(CardRendererUtils.getCardTextColor(currentPower, false, defaultTextColor, defaultTextLight)); + g.setColor(CardRendererUtils.getCardTextColor(currentPower, false, defaultTextColor, defaultTextLight, attribs.isPowerPerpetuallyAffected)); g.drawString(ptText1, ptPosStart1, curY - ptTextOffset - 1); // left // / g.setColor(defaultTextColor); g.drawString(ptText2, ptPosStart2, curY - ptTextOffset - 1); // center // t - g.setColor(CardRendererUtils.getCardTextColor(currentToughness, CardRendererUtils.isCardWithDamage(cardView), defaultTextColor, defaultTextLight)); + g.setColor(CardRendererUtils.getCardTextColor(currentToughness, CardRendererUtils.isCardWithDamage(cardView), defaultTextColor, defaultTextLight, attribs.isToughnessPerpetuallyAffected)); g.drawString(ptText3, ptPosStart3, curY - ptTextOffset - 1); // right // g.setColor(defaultTextColor); diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java b/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java index e43dfc10662d..ae50328d16a8 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java @@ -323,7 +323,7 @@ protected void drawFrame(Graphics2D g, CardPanelAttributes attribs, BufferedImag CardPanelAttributes adventureAttribs = new CardPanelAttributes( attribs.cardWidth, attribs.cardHeight, attribs.isChoosable, - attribs.isSelected, true); + attribs.isSelected, true, attribs.isPowerPerpetuallyAffected, attribs.isToughnessPerpetuallyAffected); // Draw the adventure name line box g.setPaint(getBoxColor(rightHalf.color, cardView.getCardTypes(), true)); diff --git a/Mage.Common/src/main/java/mage/view/CardView.java b/Mage.Common/src/main/java/mage/view/CardView.java index d0209e12d909..98097deee329 100644 --- a/Mage.Common/src/main/java/mage/view/CardView.java +++ b/Mage.Common/src/main/java/mage/view/CardView.java @@ -9,8 +9,11 @@ import mage.abilities.Mode; import mage.abilities.SpellAbility; import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.Effect; import mage.abilities.effects.Effects; +import mage.abilities.effects.common.continuous.BoostTargetPerpetuallyEffect; +import mage.abilities.effects.common.continuous.SetBasePowerToughnessTargetPerpetuallyEffect; import mage.abilities.hint.HintUtils; import mage.abilities.icon.CardIcon; import mage.abilities.icon.CardIconImpl; @@ -71,6 +74,8 @@ public class CardView extends SimpleCardView { protected String loyalty = ""; @Expose protected String defense = ""; + protected boolean isPowerPerpetuallyAffected = false; + protected boolean isToughnessPerpetuallyAffected = false; protected String startingLoyalty; protected String startingDefense; protected List cardTypes; @@ -346,6 +351,37 @@ public CardView(Card sourceCard, Game game, boolean showAsControlled, boolean st // TODO: research, why it used here? this.zone = cardZone; } + List perpetualEffects = game.getContinuousEffects().getPerpetuallyEffectsByCard(card, game); + if(perpetualEffects.stream() + .anyMatch(eff -> eff instanceof BoostTargetPerpetuallyEffect + || eff instanceof SetBasePowerToughnessTargetPerpetuallyEffect)) { + + if (perpetualEffects.stream() + .anyMatch(eff -> eff instanceof BoostTargetPerpetuallyEffect + && ((BoostTargetPerpetuallyEffect) eff).affectsPower(game))) { + this.isPowerPerpetuallyAffected = true; + } + + + if (perpetualEffects.stream() + .anyMatch(eff -> eff instanceof BoostTargetPerpetuallyEffect + && ((BoostTargetPerpetuallyEffect) eff).affectsToughness(game))) { + this.isToughnessPerpetuallyAffected = true; + } + + if (perpetualEffects.stream() + .anyMatch(eff -> eff instanceof SetBasePowerToughnessTargetPerpetuallyEffect + && ((SetBasePowerToughnessTargetPerpetuallyEffect) eff).affectsPower())) { + this.isPowerPerpetuallyAffected = true; + } + + + if (perpetualEffects.stream() + .anyMatch(eff -> eff instanceof SetBasePowerToughnessTargetPerpetuallyEffect + && ((SetBasePowerToughnessTargetPerpetuallyEffect) eff).affectsToughness())) { + this.isToughnessPerpetuallyAffected = true; + } + } } // FACE DOWN @@ -1213,10 +1249,16 @@ public String getPower() { return power; } + public boolean isPowerPerpetuallyAffected() { + return isPowerPerpetuallyAffected; + } public String getToughness() { return toughness; } + public boolean isToughnessPerpetuallyAffected() { + return isToughnessPerpetuallyAffected; + } public String getLoyalty() { return loyalty; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/BoostTargetPerpetuallyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/BoostTargetPerpetuallyEffect.java index a91342324d8a..67898042acd9 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/BoostTargetPerpetuallyEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/BoostTargetPerpetuallyEffect.java @@ -231,4 +231,15 @@ public void addTarget(Card card, Game game) { } + public boolean affectsPower(Game game) { + if(power == null) + return false; + return power.calculate(game, null, this) != 0; + } + + public boolean affectsToughness(Game game) { + if(toughness == null) + return false; + return toughness.calculate(game, null, this) != 0; + } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/SetBasePowerToughnessTargetPerpetuallyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/SetBasePowerToughnessTargetPerpetuallyEffect.java index eece2a040646..ab2aabbc18ad 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/SetBasePowerToughnessTargetPerpetuallyEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/SetBasePowerToughnessTargetPerpetuallyEffect.java @@ -173,4 +173,12 @@ public void addTarget(Card card, Game game) { MageObjectReference cardReference = new MageObjectReference(card, game); this.affectedObjectList.add(cardReference); } + + public boolean affectsPower() { + return power != null; + } + + public boolean affectsToughness() { + return toughness != null; + } } From 6364a3979f946c5b9904bd5928e383b3ea736457 Mon Sep 17 00:00:00 2001 From: karapuzz14 Date: Sat, 19 Oct 2024 20:45:46 +0300 Subject: [PATCH 06/12] Refactoring and small fixes --- Mage.Sets/src/mage/cards/f/FearsomeWhelp.java | 8 ++- .../src/mage/cards/p/PullOfTheMistMoon.java | 1 - .../test/cards/digital/PerpetuallyTest.java | 57 ++++++++++++++++++- .../abilities/effects/ContinuousEffects.java | 31 +++++----- .../effects/ContinuousEffectsList.java | 1 + .../CardsInYourHandPerpetuallyGainEffect.java | 10 ++-- ...ardInYourHandItPerpetuallyGainsEffect.java | 11 +++- .../GainAbilitySourcePerpetuallyEffect.java | 50 ---------------- .../GainAbilityTargetPerpetuallyEffect.java | 4 +- 9 files changed, 96 insertions(+), 77 deletions(-) delete mode 100644 Mage/src/main/java/mage/abilities/effects/common/GainAbilitySourcePerpetuallyEffect.java diff --git a/Mage.Sets/src/mage/cards/f/FearsomeWhelp.java b/Mage.Sets/src/mage/cards/f/FearsomeWhelp.java index c9ac5db4b0da..d961ea275493 100644 --- a/Mage.Sets/src/mage/cards/f/FearsomeWhelp.java +++ b/Mage.Sets/src/mage/cards/f/FearsomeWhelp.java @@ -13,7 +13,10 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; +import mage.filter.FilterCard; import mage.filter.common.FilterBySubtypeCard; +import mage.filter.common.FilterNonlandCard; +import mage.filter.predicate.mageobject.ManaValuePredicate; /** * @@ -33,9 +36,10 @@ public FearsomeWhelp(UUID ownerId, CardSetInfo setInfo) { // At the beginning of your end step, each Dragon card in your hand perpetually gains “This spell costs {1} less to cast.” Ability reduceCostAbility = new SimpleStaticAbility(new SpellCostReductionSourceEffect(1)); + FilterBySubtypeCard filter = new FilterBySubtypeCard(SubType.DRAGON); + filter.setMessage("each Dragon card in your hand"); this.addAbility(new BeginningOfYourEndStepTriggeredAbility( - new CardsInYourHandPerpetuallyGainEffect(reduceCostAbility, new FilterBySubtypeCard(SubType.DRAGON)) - .setText(" each Dragon card in your hand perpetually gains \"This spell costs {1} less to cast.\""), + new CardsInYourHandPerpetuallyGainEffect(reduceCostAbility, filter), false)); } diff --git a/Mage.Sets/src/mage/cards/p/PullOfTheMistMoon.java b/Mage.Sets/src/mage/cards/p/PullOfTheMistMoon.java index b5191540d76c..e460797a71e1 100644 --- a/Mage.Sets/src/mage/cards/p/PullOfTheMistMoon.java +++ b/Mage.Sets/src/mage/cards/p/PullOfTheMistMoon.java @@ -44,7 +44,6 @@ public PullOfTheMistMoon(UUID ownerId, CardSetInfo setInfo) { FilterPermanentCard filter = new FilterPermanentCard(); filter.add(Predicates.not(CardType.LAND.getPredicate())); - //TODO: text autogeneration? this.addAbility(new ConditionalInterveningIfTriggeredAbility( new EntersBattlefieldTriggeredAbility(new ChooseACardInYourHandItPerpetuallyGainsEffect(exileAbility, filter)), KickedCondition.ONCE, "When {this} enters, if it was kicked, choose a nonland permanent card in your hand. " + diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java index 8f5c45f4f1f0..03efc9907c0e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java @@ -447,7 +447,7 @@ public void testAftermathBothSidesAffected() { skipInitShuffling(); - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Pay 1 life: Choose a nonland card in your hand. It perpetually gains this spell costs {1} less to cast."); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Pay 1 life: Choose a nonland card in your hand. It perpetually gains \"This spell costs {1} less to cast.\""); setChoice(playerA, "Driven // Despair"); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); @@ -567,6 +567,7 @@ public void testTransformBackAffected() { // 1. Melded card becomes the target of all perpetual effects that affected its meld pair. // 2. When melded card goes into any zone except of battlefield, it becomes two cards. These cards have no perpetual effects. + // 1. Melded card becomes the target of all perpetual effects that affected its meld pair. @Test public void testMeldedCardCombinesPerpetualEffects() { @@ -613,6 +614,60 @@ public void testMeldedCardCombinesPerpetualEffects() { assertPowerToughness(playerA, "Mishra, Lost to Phyrexia", 11, 11); } + // 2. When melded card goes into any zone except of battlefield, it becomes two cards. These cards have no perpetual effects. + @Test + public void testMeldedCardRemoveEffect() { + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 15); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 10); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 10); + addCard(Zone.BATTLEFIELD, playerA, "Island", 10); + + + addCard(Zone.HAND, playerA, charger); + addCard(Zone.HAND, playerA, "Unsummon"); + addCard(Zone.HAND, playerA, "Ethereal Grasp"); + addCard(Zone.HAND, playerA, "Mishra, Claimed by Gix"); + addCard(Zone.HAND, playerA, "Phyrexian Dragon Engine"); + addCard(Zone.HAND, playerA, "Burst of Speed"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, charger, true); + setChoice(playerA, "Mishra, Claimed by Gix"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mishra, Claimed by Gix", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Phyrexian Dragon Engine", true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", "Phyrexian Dragon Engine", true); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{8}: Untap {this}."); + + + // meld cards into Mishra, Lost to Phyrexia + attack(4, playerA, "Phyrexian Dragon Engine", playerB); + attack(4, playerA, "Mishra, Claimed by Gix", playerB); + setChoice(playerA, "Phyrexian Dragon Engine"); + setModeChoice(playerA, "4"); + setModeChoice(playerA, "5"); + setModeChoice(playerA, "6"); + waitStackResolved(4, PhaseStep.DECLARE_ATTACKERS); + + castSpell(4, PhaseStep.POSTCOMBAT_MAIN, playerA, "Unsummon", "Mishra, Lost to Phyrexia", true); + + castSpell(4, PhaseStep.POSTCOMBAT_MAIN, playerA, "Mishra, Claimed by Gix", true); + + castSpell(4, PhaseStep.POSTCOMBAT_MAIN, playerA, "Phyrexian Dragon Engine", true); + + setStopAt(4, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, "Phyrexian Dragon Engine", SimpleActivatedAbility.class, 0); + assertPowerToughness(playerA, "Mishra, Claimed by Gix", 3, 5); + } + // Disturb cards test. // Perpetual effects affect both sides of disturb cards. @Test diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index ee8a46335285..69bda0967d8e 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -1403,15 +1403,15 @@ public Map> getPerpetuallyAffectedObjectsRules( } public List getPerpetuallyEffectsByCard(Card card, Game game) { List perpetuallyEffectList = new ArrayList<>(); - for(ContinuousEffect effect : layeredEffects) { - if(effect instanceof PerpetuallyEffect) { - if(((PerpetuallyEffect) effect).affectsCard(card, game)) { - perpetuallyEffectList.add(effect); - } - } - } + layeredEffects.stream() + .filter(e -> e instanceof PerpetuallyEffect) + .map(e -> (PerpetuallyEffect) e) + .filter(e -> e.affectsCard(card, game)) + .forEach(perpetuallyEffectList::add); + return perpetuallyEffectList; } + public void removePerpetuallyEffectsByCard(Card card, Game game) { List perpetuallyEffectList = getPerpetuallyEffectsByCard(card, game); for(ContinuousEffect effect : perpetuallyEffectList) { @@ -1420,16 +1420,15 @@ public void removePerpetuallyEffectsByCard(Card card, Game game) { } public boolean hasPerpetuallyEffectOn(Card card, Game game) { - for(ContinuousEffect effect : layeredEffects) { - if(effect instanceof PerpetuallyEffect) { - PerpetuallyEffect perpetuallyEffect = (PerpetuallyEffect) effect; - if(perpetuallyEffect.affectsCard(card, game)) { - return true; - } - } - } - return false; + long effectCount = layeredEffects.stream() + .filter(e -> e instanceof PerpetuallyEffect) + .map(e -> (PerpetuallyEffect) e) + .filter(e -> e.affectsCard(card, game)) + .count(); + + return effectCount > 0; } + /** * Debug only: prints out a status of the currently existing continuous effects * diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java index 1b2a8351447d..179321877654 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java @@ -147,6 +147,7 @@ private boolean isInactive(T effect, Game game) { } break; case Perpetually: + // TODO: needs discussion case OneUse: if (hasOwnerLeftGame || effect.isUsed()) { it.remove(); diff --git a/Mage/src/main/java/mage/abilities/effects/common/CardsInYourHandPerpetuallyGainEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CardsInYourHandPerpetuallyGainEffect.java index e7e91bbcce05..b917907d16cd 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CardsInYourHandPerpetuallyGainEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CardsInYourHandPerpetuallyGainEffect.java @@ -5,6 +5,7 @@ import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.continuous.GainAbilityTargetPerpetuallyEffect; +import mage.abilities.keyword.MenaceAbility; import mage.cards.Card; import mage.constants.Outcome; import mage.filter.FilterCard; @@ -23,12 +24,13 @@ public class CardsInYourHandPerpetuallyGainEffect extends OneShotEffect { public CardsInYourHandPerpetuallyGainEffect(Ability ability, FilterCard filter) { super(Outcome.AddAbility); - this.staticText = filter.getMessage() + " perpetually gain "; - if(!(ability instanceof MageSingleton)){ - this.staticText += "\"" + CardUtil.getTextWithFirstCharUpperCase(ability.getRule()) + ("\""); + this.staticText = filter.getMessage() + " perpetually"; + this.staticText += filter.getMessage().contains("cards") ? " gain " : " gains "; + if(ability instanceof MageSingleton || ability instanceof MenaceAbility) { + this.staticText += ability.getRule(); } else { - ability.getRule(); + this.staticText += "\"" + CardUtil.getTextWithFirstCharUpperCase(ability.getRule()) + "\""; } this.ability = ability; this.filter = filter; diff --git a/Mage/src/main/java/mage/abilities/effects/common/ChooseACardInYourHandItPerpetuallyGainsEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ChooseACardInYourHandItPerpetuallyGainsEffect.java index d4f33b98e99b..f1979eb0c794 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ChooseACardInYourHandItPerpetuallyGainsEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ChooseACardInYourHandItPerpetuallyGainsEffect.java @@ -1,8 +1,10 @@ package mage.abilities.effects.common; import mage.abilities.Ability; +import mage.abilities.MageSingleton; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.continuous.GainAbilityTargetPerpetuallyEffect; +import mage.abilities.keyword.MenaceAbility; import mage.cards.Card; import mage.constants.Outcome; import mage.filter.FilterCard; @@ -21,7 +23,14 @@ public class ChooseACardInYourHandItPerpetuallyGainsEffect extends OneShotEffect public ChooseACardInYourHandItPerpetuallyGainsEffect(Ability ability, FilterCard filter) { super(Outcome.AddAbility); - this.staticText = "choose " + CardUtil.addArticle(filter.getMessage()) + " in your hand. It perpetually gains " + ability.getRule(); + if(ability instanceof MageSingleton || ability instanceof MenaceAbility) { + this.staticText = "choose " + CardUtil.addArticle(filter.getMessage()) + " in your hand. " + + "It perpetually gains " + ability.getRule(); + } + else { + this.staticText = "choose " + CardUtil.addArticle(filter.getMessage()) + " in your hand. " + + "It perpetually gains \"" + CardUtil.getTextWithFirstCharUpperCase(ability.getRule()) + "\""; + } this.ability = ability; this.filter = filter; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/GainAbilitySourcePerpetuallyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/GainAbilitySourcePerpetuallyEffect.java deleted file mode 100644 index 7cb180b3e654..000000000000 --- a/Mage/src/main/java/mage/abilities/effects/common/GainAbilitySourcePerpetuallyEffect.java +++ /dev/null @@ -1,50 +0,0 @@ -package mage.abilities.effects.common; - -import mage.abilities.Ability; -import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.common.continuous.GainAbilityTargetPerpetuallyEffect; -import mage.cards.Card; -import mage.constants.Outcome; -import mage.filter.FilterCard; -import mage.game.Game; -import mage.players.Player; -import mage.target.targetpointer.FixedTarget; - -public class GainAbilitySourcePerpetuallyEffect extends OneShotEffect { - - private final Ability ability; - private final FilterCard filter; - - public GainAbilitySourcePerpetuallyEffect(Ability ability, FilterCard filter) { - super(Outcome.AddAbility); - this.staticText = "it perpetually gains " + ability.getRule(); - this.ability = ability; - this.filter = filter; - } - - protected GainAbilitySourcePerpetuallyEffect(final GainAbilitySourcePerpetuallyEffect effect) { - super(effect); - this.ability = effect.ability; - this.filter = effect.filter; - } - - @Override - public GainAbilitySourcePerpetuallyEffect copy() { - return new GainAbilitySourcePerpetuallyEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - Card card = game.getCard(source.getSourceId()); - if (!controller.getHand().isEmpty()) { - game.addEffect(new GainAbilityTargetPerpetuallyEffect(ability) - .setTargetPointer(new FixedTarget(card, game)), source); - - } - return true; - } - return false; - } -} diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetPerpetuallyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetPerpetuallyEffect.java index 5ad2d2bed0a4..fbc72faf3ae6 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetPerpetuallyEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetPerpetuallyEffect.java @@ -149,7 +149,7 @@ public boolean affectsCard(Card card, Game game) { public void removeTarget(Card card, Game game) { UUID cardId = card.getId(); morphedMap.remove(cardId); - //TODO: not correct removing? + Map> cardRulesMap = game.getState().getContinuousEffects().getPerpetuallyAffectedObjectsRules(); Set cardRefs = cardRulesMap.keySet(); cardRefs.removeIf(ref -> ref.refersTo(cardId, game)); @@ -176,7 +176,7 @@ public void addTarget(Card card, Game game) { Map> cardRulesMap = game.getState().getContinuousEffects().getPerpetuallyAffectedObjectsRules(); String rule = ability.getRule(); - String upperCaseRule = rule.substring(0, 1).toUpperCase() + rule.substring(1); + String upperCaseRule = CardUtil.getTextWithFirstCharUpperCase(rule); if (cardRulesMap.containsKey(cardReference)) { Set ruleSet = cardRulesMap.get(cardReference); From ef3ec20ce4a7845482c547622ea169831a782785 Mon Sep 17 00:00:00 2001 From: karapuzz14 Date: Sat, 19 Oct 2024 21:43:15 +0300 Subject: [PATCH 07/12] Small GUI fix? --- .../src/main/java/mage/client/game/BattlefieldPanel.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Mage.Client/src/main/java/mage/client/game/BattlefieldPanel.java b/Mage.Client/src/main/java/mage/client/game/BattlefieldPanel.java index 480bee5ffab9..d981700680a7 100644 --- a/Mage.Client/src/main/java/mage/client/game/BattlefieldPanel.java +++ b/Mage.Client/src/main/java/mage/client/game/BattlefieldPanel.java @@ -227,6 +227,11 @@ public void update(Map battlefield) { changed = true; } } + + // Check for power or toughness being perpetually affected + if(permanent.isPowerPerpetuallyAffected() || permanent.isToughnessPerpetuallyAffected()) { + changed = true; + } } oldMagePermanent.update(permanent); } From d3f3aa8c2b101dbc729ab1a4cd6d74f0d1209923 Mon Sep 17 00:00:00 2001 From: karapuzz14 Date: Sun, 20 Oct 2024 12:44:45 +0300 Subject: [PATCH 08/12] Small fixes --- .../effects/common/continuous/BoostTargetPerpetuallyEffect.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/BoostTargetPerpetuallyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/BoostTargetPerpetuallyEffect.java index 67898042acd9..354b45d8430a 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/BoostTargetPerpetuallyEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/BoostTargetPerpetuallyEffect.java @@ -180,7 +180,7 @@ public String getText(Mode mode) { return staticText; } return getTargetPointer().describeTargets(mode.getTargets(), "it") + - (getTargetPointer().isPlural(mode.getTargets()) ? " each get " : " gets ") + + (getTargetPointer().isPlural(mode.getTargets()) ? " perpetually get " : " gets ") + CardUtil.getBoostText(power, toughness, duration); } From 54ef910c0f19f2a2f8fb1e0c7d120c33fc54e617 Mon Sep 17 00:00:00 2001 From: karapuzz14 Date: Sun, 20 Oct 2024 15:19:33 +0300 Subject: [PATCH 09/12] Morph test fixes after merge --- .../org/mage/test/cards/digital/PerpetuallyTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java index 03efc9907c0e..c7b0cb5e1acc 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/digital/PerpetuallyTest.java @@ -7,6 +7,7 @@ import mage.abilities.effects.common.UntapSourceEffect; import mage.abilities.keyword.DeathtouchAbility; import mage.cards.Card; +import mage.constants.EmptyNames; import mage.constants.PhaseStep; import mage.constants.Zone; import mage.game.Game; @@ -730,13 +731,13 @@ public void testMorphGainAbilityMorph() { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unyielding Gatekeeper using Disguise", true); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", "", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", EmptyNames.FACE_DOWN_CREATURE.getTestCommand(), true); setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); - assertAbilityCount(playerA, "", SimpleActivatedAbility.class, 1); - assertAbilityCount(playerA, "", DeathtouchAbility.class, 0); + assertAbilityCount(playerA, EmptyNames.FACE_DOWN_CREATURE.getTestCommand(), SimpleActivatedAbility.class, 1); + assertAbilityCount(playerA, EmptyNames.FACE_DOWN_CREATURE.getTestCommand(), DeathtouchAbility.class, 0); } // Test hidden ability and disguise permanent ability to show after turning face up @@ -759,7 +760,7 @@ public void testMorphGainAbilityCard() { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unyielding Gatekeeper using Disguise", true); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", "", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ethereal Grasp", EmptyNames.FACE_DOWN_CREATURE.getTestCommand(), true); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}: Turn this face-down permanent face up."); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); @@ -794,7 +795,7 @@ public void testMorphBoostPTMorph() { setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); - assertPowerToughness(playerA, "", 2, 2); + assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.getTestCommand(), 2, 2); } // Test hidden boost to show From 7819c019ae79be3d5d4636d52f967a10c7127203 Mon Sep 17 00:00:00 2001 From: karapuzz14 Date: Sun, 20 Oct 2024 20:50:43 +0300 Subject: [PATCH 10/12] addOtherAbility() sourceId fix --- Mage/src/main/java/mage/game/GameState.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 209c972bce70..8144f12987b7 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -1421,9 +1421,9 @@ && getAllOtherAbilities(attachedTo.getId()).contains(ability))) { newAbility = ability.copy(); newAbility.newId(); } - if(!(attachedTo instanceof AdventureCardSpell || attachedTo instanceof SplitCardHalf)) { - newAbility.setSourceId(attachedTo.getId()); - } + + newAbility.setSourceId(attachedTo.getId()); + if(ability.getControllerId() != null) { newAbility.setControllerId(ability.getControllerId()); } From eadd4668895cc7e6547181bf4549b4d8a4acb779 Mon Sep 17 00:00:00 2001 From: karapuzz14 Date: Sun, 20 Oct 2024 21:12:23 +0300 Subject: [PATCH 11/12] Text fixes --- Mage.Sets/src/mage/cards/b/BafflingDefenses.java | 2 +- Mage.Sets/src/mage/cards/e/EtherealGrasp.java | 4 ++-- Mage.Sets/src/mage/cards/l/LumberingLightshield.java | 2 +- Mage.Sets/src/mage/cards/t/TeyoAegisAdept.java | 5 ++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Mage.Sets/src/mage/cards/b/BafflingDefenses.java b/Mage.Sets/src/mage/cards/b/BafflingDefenses.java index 73cd7de673f5..b98b4e5111cd 100644 --- a/Mage.Sets/src/mage/cards/b/BafflingDefenses.java +++ b/Mage.Sets/src/mage/cards/b/BafflingDefenses.java @@ -25,7 +25,7 @@ public BafflingDefenses(UUID ownerId, CardSetInfo setInfo) { // Target creature's base power perpetually becomes 0. Effect effect = new SetBasePowerToughnessTargetPerpetuallyEffect(StaticValue.get(0), null); - effect.setText("Target creature’s base power perpetually becomes 0"); + effect.setText("Target creature's base power perpetually becomes 0"); this.getSpellAbility().addEffect(effect); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); diff --git a/Mage.Sets/src/mage/cards/e/EtherealGrasp.java b/Mage.Sets/src/mage/cards/e/EtherealGrasp.java index bdda8ee011ab..aee508ade7c8 100644 --- a/Mage.Sets/src/mage/cards/e/EtherealGrasp.java +++ b/Mage.Sets/src/mage/cards/e/EtherealGrasp.java @@ -49,7 +49,7 @@ class EtherealGraspEffect extends OneShotEffect { EtherealGraspEffect() { super(Outcome.AddAbility); - this.staticText = "That creature perpetually gains “This creature doesn’t untap during your untap step” and “{8}: Untap this creature.”"; + this.staticText = "That creature perpetually gains \"This creature doesn't untap during your untap step\" and \"{8}: Untap this creature.\""; } private EtherealGraspEffect(final EtherealGraspEffect effect) { @@ -66,7 +66,7 @@ public boolean apply(Game game, Ability source) { game.addEffect(new GainAbilityTargetPerpetuallyEffect( new SimpleStaticAbility(Zone.BATTLEFIELD, new DontUntapInControllersUntapStepSourceEffect()), - "creature perpetually gains “This creature doesn’t untap during your untap step”" + "creature perpetually gains \"This creature doesn't untap during your untap step\"" ).setTargetPointer(new FixedTarget(source.getFirstTarget(), game)), source); game.addEffect(new GainAbilityTargetPerpetuallyEffect( diff --git a/Mage.Sets/src/mage/cards/l/LumberingLightshield.java b/Mage.Sets/src/mage/cards/l/LumberingLightshield.java index 763f7daab33b..d4c95f27404b 100644 --- a/Mage.Sets/src/mage/cards/l/LumberingLightshield.java +++ b/Mage.Sets/src/mage/cards/l/LumberingLightshield.java @@ -58,7 +58,7 @@ class LumberingLightshieldEffect extends OneShotEffect { LumberingLightshieldEffect() { super(Outcome.AddAbility); - this.staticText = "target opponent reveals a nonland card at random from their hand. It perpetually gains “This spell costs {1} more to cast.”"; + this.staticText = "target opponent reveals a nonland card at random from their hand. It perpetually gains \"This spell costs {1} more to cast.\""; } private LumberingLightshieldEffect(final LumberingLightshieldEffect effect) { diff --git a/Mage.Sets/src/mage/cards/t/TeyoAegisAdept.java b/Mage.Sets/src/mage/cards/t/TeyoAegisAdept.java index 509ec00c4371..e83873b2a282 100644 --- a/Mage.Sets/src/mage/cards/t/TeyoAegisAdept.java +++ b/Mage.Sets/src/mage/cards/t/TeyoAegisAdept.java @@ -25,7 +25,6 @@ * * @author karapuzz14 */ -// TODO: исправить GainAbility при выборе нескольких целей последовательно public final class TeyoAegisAdept extends CardImpl { public TeyoAegisAdept(UUID ownerId, CardSetInfo setInfo) { @@ -39,7 +38,7 @@ public TeyoAegisAdept(UUID ownerId, CardSetInfo setInfo) { Ability firstLoyaltyAbility = new LoyaltyAbility(new TeyoAegisAdeptFirstEffect(), 1); Ability canAttackAbility = new SimpleStaticAbility(Zone.ALL, new CanAttackAsThoughItDidntHaveDefenderSourceEffect(Duration.WhileOnBattlefield)); firstLoyaltyAbility.addEffect(new GainAbilityTargetPerpetuallyEffect(canAttackAbility) - .setText("It perpetually gains \"This creature can attack as though it didn’t have defender.\"")); + .setText("It perpetually gains \"This creature can attack as though it didn't have defender.\"")); firstLoyaltyAbility.addTarget(new TargetCreaturePermanent(0, 1)); this.addAbility(firstLoyaltyAbility); @@ -64,7 +63,7 @@ class TeyoAegisAdeptFirstEffect extends OneShotEffect { TeyoAegisAdeptFirstEffect() { super(Outcome.BoostCreature); - staticText = "Up to one target creature’s base power perpetually becomes equal to its toughness"; + staticText = "Up to one target creature's base power perpetually becomes equal to its toughness"; } private TeyoAegisAdeptFirstEffect(final TeyoAegisAdeptFirstEffect effect) { From 7a8ac4d5999e822f6c82dc61320f5d378cd9c244 Mon Sep 17 00:00:00 2001 From: karapuzz14 Date: Sun, 20 Oct 2024 22:33:41 +0300 Subject: [PATCH 12/12] Text fixes --- Mage.Sets/src/mage/cards/m/ManagorgerPhoenix.java | 1 - 1 file changed, 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/m/ManagorgerPhoenix.java b/Mage.Sets/src/mage/cards/m/ManagorgerPhoenix.java index 9eabc3a92551..9f4e8a9543bc 100644 --- a/Mage.Sets/src/mage/cards/m/ManagorgerPhoenix.java +++ b/Mage.Sets/src/mage/cards/m/ManagorgerPhoenix.java @@ -34,7 +34,6 @@ * * @author karapuzz14 */ -//TODO: learn how to improve targeting inside effect? public final class ManagorgerPhoenix extends CardImpl { public ManagorgerPhoenix(UUID ownerId, CardSetInfo setInfo) {