From 7fe6ba9c57894e108d9c1d3132ddd10ba806b6f3 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 18 Jul 2024 09:15:45 -0400 Subject: [PATCH] Implement "Promise a gift" mechanic (#12578) * [BLB] Implement Wear Down * [BLB] Implement Valley Rally * [BLB] Implement Dawn's Truce * add initial test * [BLB] Implement Kitnap * [BLB] Implement Long River's Pull * [BLB] Implement Peerless Recycling * [BLB] Implement Into the Flood Maw * add more tests * add verify skip * remove skip * a few requested changes * [BLB] Implement Nocturnal Hunger * add test for gifting a food token * [BLB] Implement Starforged Sword * add comment to activation ket * add test for adding extra cards --- Mage.Sets/src/mage/cards/d/DawnsTruce.java | 53 ++++++ .../src/mage/cards/i/IntoTheFloodMaw.java | 58 ++++++ Mage.Sets/src/mage/cards/k/Kitnap.java | 65 +++++++ .../src/mage/cards/l/LongRiversPull.java | 56 ++++++ .../src/mage/cards/n/NocturnalHunger.java | 44 +++++ .../src/mage/cards/p/PeerlessRecycling.java | 57 ++++++ .../src/mage/cards/s/StarforgedSword.java | 60 ++++++ Mage.Sets/src/mage/cards/v/ValleyRally.java | 63 +++++++ Mage.Sets/src/mage/cards/w/WearDown.java | 56 ++++++ Mage.Sets/src/mage/sets/Bloomburrow.java | 9 + .../cards/abilities/keywords/GiftTest.java | 177 ++++++++++++++++++ .../common/GiftWasPromisedCondition.java | 30 +++ .../mage/abilities/keyword/GiftAbility.java | 175 +++++++++++++++++ .../main/java/mage/constants/GiftType.java | 51 +++++ Mage/src/main/java/mage/util/CardUtil.java | 2 +- Utils/keywords.txt | 1 + 16 files changed, 956 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/d/DawnsTruce.java create mode 100644 Mage.Sets/src/mage/cards/i/IntoTheFloodMaw.java create mode 100644 Mage.Sets/src/mage/cards/k/Kitnap.java create mode 100644 Mage.Sets/src/mage/cards/l/LongRiversPull.java create mode 100644 Mage.Sets/src/mage/cards/n/NocturnalHunger.java create mode 100644 Mage.Sets/src/mage/cards/p/PeerlessRecycling.java create mode 100644 Mage.Sets/src/mage/cards/s/StarforgedSword.java create mode 100644 Mage.Sets/src/mage/cards/v/ValleyRally.java create mode 100644 Mage.Sets/src/mage/cards/w/WearDown.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/GiftTest.java create mode 100644 Mage/src/main/java/mage/abilities/condition/common/GiftWasPromisedCondition.java create mode 100644 Mage/src/main/java/mage/abilities/keyword/GiftAbility.java create mode 100644 Mage/src/main/java/mage/constants/GiftType.java diff --git a/Mage.Sets/src/mage/cards/d/DawnsTruce.java b/Mage.Sets/src/mage/cards/d/DawnsTruce.java new file mode 100644 index 00000000000..ce370569793 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DawnsTruce.java @@ -0,0 +1,53 @@ +package mage.cards.d; + +import mage.abilities.condition.common.GiftWasPromisedCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.common.AddContinuousEffectToGame; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.effects.common.continuous.GainAbilityControllerEffect; +import mage.abilities.keyword.GiftAbility; +import mage.abilities.keyword.HexproofAbility; +import mage.abilities.keyword.IndestructibleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.GiftType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DawnsTruce extends CardImpl { + + public DawnsTruce(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{W}"); + + // Gift a card + this.addAbility(new GiftAbility(this, GiftType.CARD)); + + // You and permanents you control gain hexproof until end of turn. If the gift was promised, permanents you control also gain indestructible until end of turn. + this.getSpellAbility().addEffect(new GainAbilityControllerEffect( + HexproofAbility.getInstance(), Duration.EndOfTurn + ).setText("you")); + this.getSpellAbility().addEffect(new GainAbilityControlledEffect( + HexproofAbility.getInstance(), Duration.EndOfTurn + ).concatBy("and")); + this.getSpellAbility().addEffect(new ConditionalOneShotEffect( + new AddContinuousEffectToGame(new GainAbilityControlledEffect( + IndestructibleAbility.getInstance(), Duration.EndOfTurn + )), GiftWasPromisedCondition.TRUE, "if the gift was promised, " + + "permanents you control also gain indestructible until end of turn" + )); + } + + private DawnsTruce(final DawnsTruce card) { + super(card); + } + + @Override + public DawnsTruce copy() { + return new DawnsTruce(this); + } +} diff --git a/Mage.Sets/src/mage/cards/i/IntoTheFloodMaw.java b/Mage.Sets/src/mage/cards/i/IntoTheFloodMaw.java new file mode 100644 index 00000000000..3650c548723 --- /dev/null +++ b/Mage.Sets/src/mage/cards/i/IntoTheFloodMaw.java @@ -0,0 +1,58 @@ +package mage.cards.i; + +import mage.abilities.Ability; +import mage.abilities.condition.common.GiftWasPromisedCondition; +import mage.abilities.effects.common.ReturnToHandTargetEffect; +import mage.abilities.keyword.GiftAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.GiftType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.target.TargetPermanent; +import mage.target.common.TargetOpponentsCreaturePermanent; +import mage.target.targetadjustment.TargetAdjuster; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class IntoTheFloodMaw extends CardImpl { + + public IntoTheFloodMaw(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{U}"); + + // Gift a tapped Fish + this.addAbility(new GiftAbility(this, GiftType.TAPPED_FISH)); + + // Return target creature an opponent controls to its owner's hand. If the gift was promise, instead return target nonland permanent an opponent controls to its owner's hand. + this.getSpellAbility().addEffect(new ReturnToHandTargetEffect() + .setText("return target creature an opponent controls to its owner's hand. If the gift was promise, " + + "instead return target nonland permanent an opponent controls to its owner's hand")); + this.getSpellAbility().addTarget(new TargetOpponentsCreaturePermanent()); + this.getSpellAbility().setTargetAdjuster(IntoTheFloodMawAdjuster.instance); + } + + private IntoTheFloodMaw(final IntoTheFloodMaw card) { + super(card); + } + + @Override + public IntoTheFloodMaw copy() { + return new IntoTheFloodMaw(this); + } +} + +enum IntoTheFloodMawAdjuster implements TargetAdjuster { + instance; + + @Override + public void adjustTargets(Ability ability, Game game) { + if (GiftWasPromisedCondition.TRUE.apply(game, ability)) { + ability.getTargets().clear(); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_NON_LAND)); + } + } +} diff --git a/Mage.Sets/src/mage/cards/k/Kitnap.java b/Mage.Sets/src/mage/cards/k/Kitnap.java new file mode 100644 index 00000000000..691a4d2c3f2 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/Kitnap.java @@ -0,0 +1,65 @@ +package mage.cards.k; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.GiftWasPromisedCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.TapEnchantedEffect; +import mage.abilities.effects.common.continuous.ControlEnchantedEffect; +import mage.abilities.effects.common.counter.AddCountersAttachedEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.abilities.keyword.GiftAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.GiftType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class Kitnap extends CardImpl { + + public Kitnap(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{U}{U}"); + + this.subtype.add(SubType.AURA); + + // Gift a card + this.addAbility(new GiftAbility(this, GiftType.CARD)); + + // Enchant creature + TargetPermanent auraTarget = new TargetCreaturePermanent(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + this.addAbility(new EnchantAbility(auraTarget)); + + // When Kitnap enters, tap enchanted creature. If the gift wasn't promised, put three stun counters on it. + Ability ability = new EntersBattlefieldTriggeredAbility(new TapEnchantedEffect()); + ability.addEffect(new ConditionalOneShotEffect( + new AddCountersAttachedEffect(CounterType.STUN.createInstance(3), ""), + GiftWasPromisedCondition.FALSE, "if the gift wasn't promised, put three stun counters on it" + )); + this.addAbility(ability); + + // You control enchanted creature. + this.addAbility(new SimpleStaticAbility(new ControlEnchantedEffect())); + } + + private Kitnap(final Kitnap card) { + super(card); + } + + @Override + public Kitnap copy() { + return new Kitnap(this); + } +} diff --git a/Mage.Sets/src/mage/cards/l/LongRiversPull.java b/Mage.Sets/src/mage/cards/l/LongRiversPull.java new file mode 100644 index 00000000000..36d5178fc0c --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LongRiversPull.java @@ -0,0 +1,56 @@ +package mage.cards.l; + +import mage.abilities.Ability; +import mage.abilities.condition.common.GiftWasPromisedCondition; +import mage.abilities.effects.common.CounterTargetEffect; +import mage.abilities.keyword.GiftAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.GiftType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.target.TargetSpell; +import mage.target.targetadjustment.TargetAdjuster; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class LongRiversPull extends CardImpl { + + public LongRiversPull(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{U}{U}"); + + // Gift a card + this.addAbility(new GiftAbility(this, GiftType.CARD)); + + // Counter target creature spell. If the gift was promised, instead counter target spell. + this.getSpellAbility().addEffect(new CounterTargetEffect() + .setText("counter target creature spell. If the gift was promised, instead counter target spell")); + this.getSpellAbility().addTarget(new TargetSpell(StaticFilters.FILTER_SPELL_CREATURE)); + this.getSpellAbility().setTargetAdjuster(LongRiversPullAdjuster.instance); + } + + private LongRiversPull(final LongRiversPull card) { + super(card); + } + + @Override + public LongRiversPull copy() { + return new LongRiversPull(this); + } +} + +enum LongRiversPullAdjuster implements TargetAdjuster { + instance; + + @Override + public void adjustTargets(Ability ability, Game game) { + if (GiftWasPromisedCondition.TRUE.apply(game, ability)) { + ability.getTargets().clear(); + ability.addTarget(new TargetSpell()); + } + } +} diff --git a/Mage.Sets/src/mage/cards/n/NocturnalHunger.java b/Mage.Sets/src/mage/cards/n/NocturnalHunger.java new file mode 100644 index 00000000000..baa31716e5b --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NocturnalHunger.java @@ -0,0 +1,44 @@ +package mage.cards.n; + +import mage.abilities.condition.common.GiftWasPromisedCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; +import mage.abilities.keyword.GiftAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.GiftType; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class NocturnalHunger extends CardImpl { + + public NocturnalHunger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{B}"); + + // Gift a Food + this.addAbility(new GiftAbility(this, GiftType.FOOD)); + + // Destroy target creature. If the gift wasn't promised, you lose 2 life. + this.getSpellAbility().addEffect(new DestroyTargetEffect()); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + this.getSpellAbility().addEffect(new ConditionalOneShotEffect( + new LoseLifeSourceControllerEffect(2), GiftWasPromisedCondition.FALSE, + "if the gift wasn't promised, you lose 2 life" + )); + } + + private NocturnalHunger(final NocturnalHunger card) { + super(card); + } + + @Override + public NocturnalHunger copy() { + return new NocturnalHunger(this); + } +} diff --git a/Mage.Sets/src/mage/cards/p/PeerlessRecycling.java b/Mage.Sets/src/mage/cards/p/PeerlessRecycling.java new file mode 100644 index 00000000000..27aecf869eb --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PeerlessRecycling.java @@ -0,0 +1,57 @@ +package mage.cards.p; + +import mage.abilities.Ability; +import mage.abilities.condition.common.GiftWasPromisedCondition; +import mage.abilities.effects.common.ReturnFromGraveyardToHandTargetEffect; +import mage.abilities.keyword.GiftAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.GiftType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.target.common.TargetCardInYourGraveyard; +import mage.target.targetadjustment.TargetAdjuster; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class PeerlessRecycling extends CardImpl { + + public PeerlessRecycling(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{G}"); + + // Gift a card + this.addAbility(new GiftAbility(this, GiftType.CARD)); + + // Return target permanent from your graveyard to your hand. If the gift was promised, instead return two target permanent cards from your graveyard to your hand. + this.getSpellAbility().addEffect(new ReturnFromGraveyardToHandTargetEffect() + .setText("return target permanent from your graveyard to your hand. If the gift was promised, " + + "instead return two target permanent cards from your graveyard to your hand")); + this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_PERMANENT)); + this.getSpellAbility().setTargetAdjuster(PeerlessRecyclingAdjuster.instance); + } + + private PeerlessRecycling(final PeerlessRecycling card) { + super(card); + } + + @Override + public PeerlessRecycling copy() { + return new PeerlessRecycling(this); + } +} + +enum PeerlessRecyclingAdjuster implements TargetAdjuster { + instance; + + @Override + public void adjustTargets(Ability ability, Game game) { + if (GiftWasPromisedCondition.TRUE.apply(game, ability)) { + ability.getTargets().clear(); + ability.addTarget(new TargetCardInYourGraveyard(2, StaticFilters.FILTER_CARD_PERMANENTS)); + } + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/StarforgedSword.java b/Mage.Sets/src/mage/cards/s/StarforgedSword.java new file mode 100644 index 00000000000..d2dd989d3e1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StarforgedSword.java @@ -0,0 +1,60 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAttachToTarget; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.GiftWasPromisedCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.continuous.BoostEquippedEffect; +import mage.abilities.effects.common.continuous.LoseAbilityAttachedEffect; +import mage.abilities.keyword.EquipAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.GiftAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AttachmentType; +import mage.constants.CardType; +import mage.constants.GiftType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StarforgedSword extends CardImpl { + + public StarforgedSword(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}"); + + this.subtype.add(SubType.EQUIPMENT); + + // Gift a tapped Fish + this.addAbility(new GiftAbility(this, GiftType.TAPPED_FISH)); + + // When Starforged Sword enters, if the gift was promised, attach Starforged Sword to target creature you control. + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new EntersBattlefieldAttachToTarget(), GiftWasPromisedCondition.TRUE, + "When {this} enters, if the gift was promised, attach {this} to target creature you control." + )); + + // Equipped creature gets +3/+3 and loses flying. + Ability ability = new SimpleStaticAbility(new BoostEquippedEffect(3, 3)); + ability.addEffect(new LoseAbilityAttachedEffect( + FlyingAbility.getInstance(), AttachmentType.EQUIPMENT + ).setText("and loses flying")); + this.addAbility(ability); + + // Equip {3} + this.addAbility(new EquipAbility(3)); + } + + private StarforgedSword(final StarforgedSword card) { + super(card); + } + + @Override + public StarforgedSword copy() { + return new StarforgedSword(this); + } +} diff --git a/Mage.Sets/src/mage/cards/v/ValleyRally.java b/Mage.Sets/src/mage/cards/v/ValleyRally.java new file mode 100644 index 00000000000..183d6d13822 --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/ValleyRally.java @@ -0,0 +1,63 @@ +package mage.cards.v; + +import mage.abilities.Ability; +import mage.abilities.condition.common.GiftWasPromisedCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.common.AddContinuousEffectToGame; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.abilities.keyword.GiftAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.GiftType; +import mage.game.Game; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.targetadjustment.TargetAdjuster; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ValleyRally extends CardImpl { + + public ValleyRally(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{R}"); + + // Gift a Food + this.addAbility(new GiftAbility(this, GiftType.FOOD)); + + // Creatures you control get +2/+0 until end of turn. If the gift was promised, target creature you control gains first strike until end of turn. + this.getSpellAbility().addEffect(new BoostControlledEffect(2, 0, Duration.EndOfTurn)); + this.getSpellAbility().addEffect(new ConditionalOneShotEffect( + new AddContinuousEffectToGame(new GainAbilityTargetEffect(FirstStrikeAbility.getInstance())), + GiftWasPromisedCondition.TRUE, "If the gift was promised, target creature " + + "you control gains first strike until end of turn" + )); + this.getSpellAbility().setTargetAdjuster(ValleyRallyAdjuster.instance); + } + + private ValleyRally(final ValleyRally card) { + super(card); + } + + @Override + public ValleyRally copy() { + return new ValleyRally(this); + } +} + +enum ValleyRallyAdjuster implements TargetAdjuster { + instance; + + @Override + public void adjustTargets(Ability ability, Game game) { + if (GiftWasPromisedCondition.TRUE.apply(game, ability)) { + ability.getTargets().clear(); + ability.addTarget(new TargetControlledCreaturePermanent()); + } + } +} diff --git a/Mage.Sets/src/mage/cards/w/WearDown.java b/Mage.Sets/src/mage/cards/w/WearDown.java new file mode 100644 index 00000000000..4ce7894d6c5 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WearDown.java @@ -0,0 +1,56 @@ +package mage.cards.w; + +import mage.abilities.Ability; +import mage.abilities.condition.common.GiftWasPromisedCondition; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.keyword.GiftAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.GiftType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.target.TargetPermanent; +import mage.target.targetadjustment.TargetAdjuster; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WearDown extends CardImpl { + + public WearDown(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{G}"); + + // Gift a card + this.addAbility(new GiftAbility(this, GiftType.CARD)); + + // Destroy target artifact or enchantment. If the gift was promised, instead destroy two target artifacts and/or enchantments. + this.getSpellAbility().addEffect(new DestroyTargetEffect().setText("destroy target artifact or enchantment. " + + "If the gift was promised, instead destroy two target artifacts and/or enchantments")); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_ARTIFACT_OR_ENCHANTMENT)); + this.getSpellAbility().setTargetAdjuster(WearDownAdjuster.instance); + } + + private WearDown(final WearDown card) { + super(card); + } + + @Override + public WearDown copy() { + return new WearDown(this); + } +} + +enum WearDownAdjuster implements TargetAdjuster { + instance; + + @Override + public void adjustTargets(Ability ability, Game game) { + if (GiftWasPromisedCondition.TRUE.apply(game, ability)) { + ability.getTargets().clear(); + ability.addTarget(new TargetPermanent(2, StaticFilters.FILTER_PERMANENT_ARTIFACT_OR_ENCHANTMENT)); + } + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/Bloomburrow.java b/Mage.Sets/src/mage/sets/Bloomburrow.java index a21f25be371..583ae8721b4 100644 --- a/Mage.Sets/src/mage/sets/Bloomburrow.java +++ b/Mage.Sets/src/mage/sets/Bloomburrow.java @@ -55,6 +55,7 @@ public final class Bloomburrow extends ExpansionSet { cards.add(new SetCardInfo("Coruscation Mage", 131, Rarity.UNCOMMON, mage.cards.c.CoruscationMage.class)); cards.add(new SetCardInfo("Curious Forager", 169, Rarity.UNCOMMON, mage.cards.c.CuriousForager.class)); cards.add(new SetCardInfo("Darkstar Augur", 90, Rarity.RARE, mage.cards.d.DarkstarAugur.class)); + cards.add(new SetCardInfo("Dawn's Truce", 9, Rarity.RARE, mage.cards.d.DawnsTruce.class)); cards.add(new SetCardInfo("Diresight", 91, Rarity.COMMON, mage.cards.d.Diresight.class)); cards.add(new SetCardInfo("Downwind Ambusher", 92, Rarity.UNCOMMON, mage.cards.d.DownwindAmbusher.class)); cards.add(new SetCardInfo("Dreamdew Entrancer", 211, Rarity.RARE, mage.cards.d.DreamdewEntrancer.class)); @@ -92,15 +93,18 @@ public final class Bloomburrow extends ExpansionSet { cards.add(new SetCardInfo("Hugs, Grisly Guardian", 218, Rarity.MYTHIC, mage.cards.h.HugsGrislyGuardian.class)); cards.add(new SetCardInfo("Hunter's Talent", 179, Rarity.UNCOMMON, mage.cards.h.HuntersTalent.class)); cards.add(new SetCardInfo("Innkeeper's Talent", 180, Rarity.RARE, mage.cards.i.InnkeepersTalent.class)); + cards.add(new SetCardInfo("Into the Flood Maw", 52, Rarity.UNCOMMON, mage.cards.i.IntoTheFloodMaw.class)); cards.add(new SetCardInfo("Iridescent Vinelasher", 99, Rarity.RARE, mage.cards.i.IridescentVinelasher.class)); cards.add(new SetCardInfo("Island", 266, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Junkblade Bruiser", 220, Rarity.COMMON, mage.cards.j.JunkbladeBruiser.class)); cards.add(new SetCardInfo("Kindlespark Duo", 142, Rarity.COMMON, mage.cards.k.KindlesparkDuo.class)); + cards.add(new SetCardInfo("Kitnap", 53, Rarity.RARE, mage.cards.k.Kitnap.class)); cards.add(new SetCardInfo("Kitsa, Otterball Elite", 54, Rarity.MYTHIC, mage.cards.k.KitsaOtterballElite.class)); cards.add(new SetCardInfo("Knightfisher", 55, Rarity.UNCOMMON, mage.cards.k.Knightfisher.class)); cards.add(new SetCardInfo("Lifecreed Duo", 20, Rarity.COMMON, mage.cards.l.LifecreedDuo.class)); cards.add(new SetCardInfo("Lightshell Duo", 56, Rarity.COMMON, mage.cards.l.LightshellDuo.class)); cards.add(new SetCardInfo("Lilypad Village", 255, Rarity.UNCOMMON, mage.cards.l.LilypadVillage.class)); + cards.add(new SetCardInfo("Long River's Pull", 58, Rarity.UNCOMMON, mage.cards.l.LongRiversPull.class)); cards.add(new SetCardInfo("Lumra, Bellow of the Woods", 183, Rarity.MYTHIC, mage.cards.l.LumraBellowOfTheWoods.class)); cards.add(new SetCardInfo("Lunar Convocation", 223, Rarity.RARE, mage.cards.l.LunarConvocation.class)); cards.add(new SetCardInfo("Lupinflower Village", 256, Rarity.UNCOMMON, mage.cards.l.LupinflowerVillage.class)); @@ -118,11 +122,13 @@ public final class Bloomburrow extends ExpansionSet { cards.add(new SetCardInfo("Muerra, Trash Tactician", 227, Rarity.RARE, mage.cards.m.MuerraTrashTactician.class)); cards.add(new SetCardInfo("Nettle Guard", 23, Rarity.COMMON, mage.cards.n.NettleGuard.class)); cards.add(new SetCardInfo("Nightwhorl Hermit", 62, Rarity.COMMON, mage.cards.n.NightwhorlHermit.class)); + cards.add(new SetCardInfo("Nocturnal Hunger", 102, Rarity.COMMON, mage.cards.n.NocturnalHunger.class)); cards.add(new SetCardInfo("Oakhollow Village", 258, Rarity.UNCOMMON, mage.cards.o.OakhollowVillage.class)); cards.add(new SetCardInfo("Overprotect", 185, Rarity.UNCOMMON, mage.cards.o.Overprotect.class)); cards.add(new SetCardInfo("Patchwork Banner", 247, Rarity.UNCOMMON, mage.cards.p.PatchworkBanner.class)); cards.add(new SetCardInfo("Pawpatch Formation", 186, Rarity.UNCOMMON, mage.cards.p.PawpatchFormation.class)); cards.add(new SetCardInfo("Pearl of Wisdom", 64, Rarity.COMMON, mage.cards.p.PearlOfWisdom.class)); + cards.add(new SetCardInfo("Peerless Recycling", 188, Rarity.UNCOMMON, mage.cards.p.PeerlessRecycling.class)); cards.add(new SetCardInfo("Pileated Provisioner", 25, Rarity.COMMON, mage.cards.p.PileatedProvisioner.class)); cards.add(new SetCardInfo("Plains", 262, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Playful Shove", 145, Rarity.UNCOMMON, mage.cards.p.PlayfulShove.class)); @@ -149,6 +155,7 @@ public final class Bloomburrow extends ExpansionSet { cards.add(new SetCardInfo("Sinister Monolith", 113, Rarity.UNCOMMON, mage.cards.s.SinisterMonolith.class)); cards.add(new SetCardInfo("Spellgyre", 72, Rarity.UNCOMMON, mage.cards.s.Spellgyre.class)); cards.add(new SetCardInfo("Splash Lasher", 73, Rarity.UNCOMMON, mage.cards.s.SplashLasher.class)); + cards.add(new SetCardInfo("Starforged Sword", 249, Rarity.UNCOMMON, mage.cards.s.StarforgedSword.class)); cards.add(new SetCardInfo("Star Charter", 33, Rarity.UNCOMMON, mage.cards.s.StarCharter.class)); cards.add(new SetCardInfo("Stargaze", 114, Rarity.UNCOMMON, mage.cards.s.Stargaze.class)); cards.add(new SetCardInfo("Starlit Soothsayer", 115, Rarity.COMMON, mage.cards.s.StarlitSoothsayer.class)); @@ -178,6 +185,7 @@ public final class Bloomburrow extends ExpansionSet { cards.add(new SetCardInfo("Treetop Sentries", 201, Rarity.COMMON, mage.cards.t.TreetopSentries.class)); cards.add(new SetCardInfo("Uncharted Haven", 261, Rarity.COMMON, mage.cards.u.UnchartedHaven.class)); cards.add(new SetCardInfo("Valley Mightcaller", 202, Rarity.RARE, mage.cards.v.ValleyMightcaller.class)); + cards.add(new SetCardInfo("Valley Rally", 159, Rarity.UNCOMMON, mage.cards.v.ValleyRally.class)); cards.add(new SetCardInfo("Valley Rotcaller", 119, Rarity.RARE, mage.cards.v.ValleyRotcaller.class)); cards.add(new SetCardInfo("Veteran Guardmouse", 237, Rarity.COMMON, mage.cards.v.VeteranGuardmouse.class)); cards.add(new SetCardInfo("Vinereap Mentor", 238, Rarity.UNCOMMON, mage.cards.v.VinereapMentor.class)); @@ -185,6 +193,7 @@ public final class Bloomburrow extends ExpansionSet { cards.add(new SetCardInfo("War Squeak", 160, Rarity.COMMON, mage.cards.w.WarSqueak.class)); cards.add(new SetCardInfo("Warren Elder", 37, Rarity.COMMON, mage.cards.w.WarrenElder.class)); cards.add(new SetCardInfo("Warren Warleader", 38, Rarity.MYTHIC, mage.cards.w.WarrenWarleader.class)); + cards.add(new SetCardInfo("Wear Down", 203, Rarity.UNCOMMON, mage.cards.w.WearDown.class)); cards.add(new SetCardInfo("Whiskerquill Scribe", 161, Rarity.COMMON, mage.cards.w.WhiskerquillScribe.class)); cards.add(new SetCardInfo("Whiskervale Forerunner", 40, Rarity.RARE, mage.cards.w.WhiskervaleForerunner.class)); cards.add(new SetCardInfo("Zoraline, Cosmos Caller", 242, Rarity.RARE, mage.cards.z.ZoralineCosmosCaller.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/GiftTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/GiftTest.java new file mode 100644 index 00000000000..7f85d256f0a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/GiftTest.java @@ -0,0 +1,177 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.abilities.keyword.FirstStrikeAbility; +import mage.abilities.keyword.HexproofAbility; +import mage.abilities.keyword.IndestructibleAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class GiftTest extends CardTestPlayerBase { + + private static final String truce = "Dawn's Truce"; + + @Test + public void testNoGift() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + addCard(Zone.HAND, playerA, truce); + + setChoice(playerA, false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, truce); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAbility(playerA, "Plains", HexproofAbility.getInstance(), true, 2); + assertAbility(playerA, "Plains", IndestructibleAbility.getInstance(), false, 2); + assertHandCount(playerB, 0); + } + + @Test + public void testGift() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + addCard(Zone.HAND, playerA, truce); + + setChoice(playerA, true); + setChoice(playerA, playerB.getName()); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, truce); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAbility(playerA, "Plains", HexproofAbility.getInstance(), true, 2); + assertAbility(playerA, "Plains", IndestructibleAbility.getInstance(), true, 2); + assertHandCount(playerB, 1); + } + + private static final String kitnap = "Kitnap"; + private static final String bear = "Grizzly Bears"; + + @Test + public void testPermanentNoGift() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + addCard(Zone.BATTLEFIELD, playerB, bear); + addCard(Zone.HAND, playerA, kitnap); + + setChoice(playerA, false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, kitnap, bear); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, kitnap, 1); + assertPermanentCount(playerA, bear, 1); + assertTapped(bear, true); + assertCounterCount(bear, CounterType.STUN, 3); + assertHandCount(playerB, 0); + } + + @Test + public void testPermanentGift() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + addCard(Zone.BATTLEFIELD, playerB, bear); + addCard(Zone.HAND, playerA, kitnap); + + setChoice(playerA, true); + setChoice(playerA, playerB.getName()); + setChoice(playerA, "When"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, kitnap, bear); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, kitnap, 1); + assertPermanentCount(playerA, bear, 1); + assertTapped(bear, true); + assertCounterCount(bear, CounterType.STUN, 0); + assertHandCount(playerB, 1); + } + + private static final String hunger = "Nocturnal Hunger"; + + @Test + public void testNoGiftToken() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.BATTLEFIELD, playerB, bear); + addCard(Zone.HAND, playerA, hunger); + + setChoice(playerA, false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hunger, bear); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertGraveyardCount(playerA, hunger, 1); + assertGraveyardCount(playerB, bear, 1); + assertPermanentCount(playerB, "Food Token", 0); + } + + @Test + public void testGiftToken() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.BATTLEFIELD, playerB, bear); + addCard(Zone.HAND, playerA, hunger); + + setChoice(playerA, true); + setChoice(playerA, playerB.getName()); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hunger, bear); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertGraveyardCount(playerA, hunger, 1); + assertGraveyardCount(playerB, bear, 1); + assertPermanentCount(playerB, "Food Token", 1); + } + + private static final String rally = "Valley Rally"; + + @Test + public void testNoGiftExtraTarget() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.BATTLEFIELD, playerA, bear); + addCard(Zone.HAND, playerA, rally); + + setChoice(playerA, false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rally); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, bear, 2 + 2, 2 + 0); + assertAbility(playerA, bear, FirstStrikeAbility.getInstance(), false); + assertPermanentCount(playerB, "Food Token", 0); + } + + @Test + public void testGiftExtraTarget() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.BATTLEFIELD, playerA, bear); + addCard(Zone.HAND, playerA, rally); + + setChoice(playerA, true); + setChoice(playerA, playerB.getName()); + addTarget(playerA, bear); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rally); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, bear, 2 + 2, 2 + 0); + assertAbility(playerA, bear, FirstStrikeAbility.getInstance(), true); + assertPermanentCount(playerB, "Food Token", 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/GiftWasPromisedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/GiftWasPromisedCondition.java new file mode 100644 index 00000000000..1cf353240c5 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/GiftWasPromisedCondition.java @@ -0,0 +1,30 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.keyword.GiftAbility; +import mage.game.Game; +import mage.util.CardUtil; + +/** + * @author TheElk801 + */ +public enum GiftWasPromisedCondition implements Condition { + TRUE(true), + FALSE(false); + private final boolean flag; + + GiftWasPromisedCondition(boolean flag) { + this.flag = flag; + } + + @Override + public boolean apply(Game game, Ability source) { + return flag == CardUtil.checkSourceCostsTagExists(game, source, GiftAbility.GIFT_ACTIVATION_VALUE_KEY); + } + + @Override + public String toString() { + return "Gift was " + (flag ? "" : "not ") + "promised"; + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/GiftAbility.java b/Mage/src/main/java/mage/abilities/keyword/GiftAbility.java new file mode 100644 index 00000000000..8bc17af7677 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/GiftAbility.java @@ -0,0 +1,175 @@ +package mage.abilities.keyword; + +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.StaticAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.GiftWasPromisedCondition; +import mage.abilities.costs.*; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.constants.GiftType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetPlayer; +import mage.target.common.TargetOpponent; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public class GiftAbility extends StaticAbility implements OptionalAdditionalSourceCosts { + + private static final String keywordText = "Gift"; + private static final String reminderText = "You may promise an opponent a gift as you cast this spell. If you do, "; + private final String rule; + + // this is set to null first when the player chooses to use it then set to the playerId later once the player is chosen + public static final String GIFT_ACTIVATION_VALUE_KEY = "giftPromisedActivation"; + + protected OptionalAdditionalCost additionalCost; + private final GiftType giftType; + + private static String makeReminderText(GiftType giftType, boolean isPermanent) { + return reminderText + (isPermanent ? "when it enters, " : "") + "they " + + giftType.getDescription() + (isPermanent ? "." : " before its other effects."); + } + + public GiftAbility(Card card, GiftType giftType) { + super(Zone.STACK, null); + this.additionalCost = new OptionalAdditionalCostImpl( + keywordText + ' ' + giftType, + makeReminderText(giftType, card.isPermanent()), + new PromiseGiftCost(giftType) + ); + this.additionalCost.setRepeatable(false); + this.giftType = giftType; + this.rule = additionalCost.getName() + ' ' + additionalCost.getReminderText(); + this.setRuleAtTheTop(true); + if (card.isPermanent()) { + this.addSubAbility(new ConditionalInterveningIfTriggeredAbility( + new EntersBattlefieldTriggeredAbility(new PromiseGiftEffect(giftType)), + GiftWasPromisedCondition.TRUE, "When this permanent enters, " + + "if the gift was promised, they " + giftType.getDescription() + '.' + ).setRuleVisible(false)); + } else { + card.getSpellAbility().addEffect(new PromiseGiftEffect(giftType)); + } + } + + private GiftAbility(final GiftAbility ability) { + super(ability); + this.rule = ability.rule; + this.additionalCost = ability.additionalCost.copy(); + this.giftType = ability.giftType; + } + + @Override + public GiftAbility copy() { + return new GiftAbility(this); + } + + @Override + public void addOptionalAdditionalCosts(Ability ability, Game game) { + if (!(ability instanceof SpellAbility)) { + return; + } + Player player = game.getPlayer(ability.getControllerId()); + if (player == null) { + return; + } + additionalCost.reset(); + if (!additionalCost.canPay(ability, this, ability.getControllerId(), game) + || !player.chooseUse(Outcome.Benefit, "Promise an opponent " + giftType.getName() + '?', ability, game)) { + return; + } + additionalCost.activate(); + for (Cost cost : ((Costs) additionalCost)) { + ability.getCosts().add(cost.copy()); + } + ability.setCostsTag(GIFT_ACTIVATION_VALUE_KEY, null); + } + + @Override + public String getCastMessageSuffix() { + return additionalCost.getCastSuffixMessage(0); + } + + @Override + public String getRule() { + return rule; + } +} + +class PromiseGiftCost extends CostImpl { + + PromiseGiftCost(GiftType giftType) { + text = "Gift " + giftType.getName(); + } + + private PromiseGiftCost(final PromiseGiftCost cost) { + super(cost); + } + + @Override + public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { + return true; + } + + @Override + public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { + Player player = game.getPlayer(controllerId); + if (player == null) { + return paid; + } + TargetPlayer target = new TargetOpponent(); + target.withNotTarget(true); + player.choose(Outcome.Detriment, target, source, game); + Player opponent = game.getPlayer(target.getFirstTarget()); + if (opponent == null) { + return paid; + } + source.setCostsTag(GiftAbility.GIFT_ACTIVATION_VALUE_KEY, opponent.getId()); + paid = true; + return paid; + } + + @Override + public PromiseGiftCost copy() { + return new PromiseGiftCost(this); + } + +} + +class PromiseGiftEffect extends OneShotEffect { + + private final GiftType giftType; + + PromiseGiftEffect(GiftType giftType) { + super(Outcome.Benefit); + this.giftType = giftType; + } + + private PromiseGiftEffect(final PromiseGiftEffect effect) { + super(effect); + this.giftType = effect.giftType; + } + + @Override + public PromiseGiftEffect copy() { + return new PromiseGiftEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(CardUtil.getSourceCostsTag( + game, source, GiftAbility.GIFT_ACTIVATION_VALUE_KEY, (UUID) null + )); + return player != null && giftType.applyGift(player, game, source); + } +} diff --git a/Mage/src/main/java/mage/constants/GiftType.java b/Mage/src/main/java/mage/constants/GiftType.java new file mode 100644 index 00000000000..4363422f106 --- /dev/null +++ b/Mage/src/main/java/mage/constants/GiftType.java @@ -0,0 +1,51 @@ +package mage.constants; + +import mage.abilities.Ability; +import mage.game.Game; +import mage.game.permanent.token.FishNoAbilityToken; +import mage.game.permanent.token.FoodToken; +import mage.players.Player; + +/** + * @author TheElk801 + */ +public enum GiftType { + CARD( + "a card", "draw a card", + (p, g, s) -> p.drawCards(1, s, g) > 0 + ), + FOOD( + "a Food", "create a Food token", + (p, g, s) -> new FoodToken().putOntoBattlefield(1, g, s, p.getId()) + ), + TAPPED_FISH( + "a tapped Fish", "create a tapped 1/1 blue Fish creature token", + (p, g, s) -> new FishNoAbilityToken().putOntoBattlefield(1, g, s, p.getId(), true, false) + ); + + private interface GiftResolver { + boolean apply(Player player, Game game, Ability source); + } + + private final String name; + private final String description; + private final GiftResolver giftResolver; + + GiftType(String name, String description, GiftResolver giftResolver) { + this.name = name; + this.description = description; + this.giftResolver = giftResolver; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public boolean applyGift(Player player, Game game, Ability source) { + return giftResolver.apply(player, game, source); + } +} diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 9c9b75cfaac..7c6a877734f 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1791,7 +1791,7 @@ public final class CardUtil { if (value == null) { throw new IllegalStateException("Wrong code usage: Costs tag " + tag + " has value stored of type null but is trying to be read. Use checkSourceCostsTagExists"); } - if (value.getClass() != defaultValue.getClass()) { + if (defaultValue != null && value.getClass() != defaultValue.getClass()) { throw new IllegalStateException("Wrong code usage: Costs tag " + tag + " has value stored of type " + value.getClass().getName() + " different from default of type " + defaultValue.getClass().getName()); } return (T) value; diff --git a/Utils/keywords.txt b/Utils/keywords.txt index aaa9cbd7d67..bf7cf91f042 100644 --- a/Utils/keywords.txt +++ b/Utils/keywords.txt @@ -61,6 +61,7 @@ Foretell|card, manaString| For Mirrodin!|new| Freerunning|manaString| Friends forever|instance| +Gift|card| Haste|instance| Hexproof|instance| Hideaway|number|