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
This commit is contained in:
Evan Kranzler 2024-07-18 09:15:45 -04:00 committed by GitHub
parent 0d3590e579
commit 7fe6ba9c57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 956 additions and 1 deletions

View file

@ -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);
}
}

View file

@ -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));
}
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}
}

View file

@ -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));
}
}
}

View file

@ -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));

View file

@ -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);
}
}

View file

@ -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";
}
}

View file

@ -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<Cost>) 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);
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -61,6 +61,7 @@ Foretell|card, manaString|
For Mirrodin!|new|
Freerunning|manaString|
Friends forever|instance|
Gift|card|
Haste|instance|
Hexproof|instance|
Hideaway|number|