diff --git a/Mage.Sets/src/mage/cards/z/ZinniaValleysVoice.java b/Mage.Sets/src/mage/cards/z/ZinniaValleysVoice.java new file mode 100644 index 000000000000..e19062bc9d89 --- /dev/null +++ b/Mage.Sets/src/mage/cards/z/ZinniaValleysVoice.java @@ -0,0 +1,90 @@ +package mage.cards.z; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledSpellsEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.OffspringAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.common.FilterNonlandCard; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.filter.predicate.mageobject.BasePowerPredicate; +import java.util.UUID; + +import static mage.abilities.dynamicvalue.common.StaticValue.get; +import static mage.constants.Duration.WhileOnBattlefield; + + +/** + * Zinnia, Valley's Voice + * + * Legendary Creature — Bird Bard + * + * Flying + * Zinnia, Valley's Voice gets +X/+0, where X is the number of other creatures + * you control with base power 1. + * Creature spells you cast have offspring {2}. + * + * @author DreamWaker and sneddigrolyat + */ +public final class ZinniaValleysVoice extends CardImpl { + + // "other creatures you control with base power 1" + private static final FilterCreaturePermanent bfilter = new FilterCreaturePermanent("other creatures you control with base power 1"); + + static { + bfilter.add(new BasePowerPredicate(ComparisonType.EQUAL_TO, 1)); + bfilter.add(TargetController.YOU.getControllerPredicate()); + bfilter.add(AnotherPredicate.instance); + } + + private static final PermanentsOnBattlefieldCount bcount = new PermanentsOnBattlefieldCount(bfilter); + + // "creature spells you cast" + static final FilterNonlandCard cfilter = new FilterNonlandCard("creature spells you cast"); + + static { + cfilter.add(CardType.CREATURE.getPredicate()); + } + + + public ZinniaValleysVoice(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{R}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.BIRD); + this.subtype.add(SubType.BARD); + this.power = new MageInt(1); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Creature spells you cast have offspring {2}. + this.addAbility(new SimpleStaticAbility( + new GainAbilityControlledSpellsEffect(new OffspringAbility("{2}"), cfilter) + .setText("Creature spells you cast have offspring {2}.") + )); + + // Zinnia, Valley's Voice gets +X/+0, + // where X is the number of other creatures you control with base power 1. + this.addAbility(new SimpleStaticAbility( + new BoostSourceEffect(bcount, get(0), WhileOnBattlefield) + )); + } + + private ZinniaValleysVoice(final ZinniaValleysVoice card) { + super(card); + } + + @Override + public ZinniaValleysVoice copy() { + return new ZinniaValleysVoice(this); + } +} + diff --git a/Mage.Sets/src/mage/sets/BloomburrowCommander.java b/Mage.Sets/src/mage/sets/BloomburrowCommander.java index 4db413a3a5a8..0fc947a4f81a 100644 --- a/Mage.Sets/src/mage/sets/BloomburrowCommander.java +++ b/Mage.Sets/src/mage/sets/BloomburrowCommander.java @@ -371,6 +371,7 @@ private BloomburrowCommander() { cards.add(new SetCardInfo("Wooded Ridgeline", 353, Rarity.COMMON, mage.cards.w.WoodedRidgeline.class)); cards.add(new SetCardInfo("Woodland Cemetery", 354, Rarity.RARE, mage.cards.w.WoodlandCemetery.class)); cards.add(new SetCardInfo("Yavimaya Coast", 355, Rarity.RARE, mage.cards.y.YavimayaCoast.class)); + cards.add(new SetCardInfo("Zinnia, Valley's Voice", 4, Rarity.MYTHIC, mage.cards.z.ZinniaValleysVoice.class)); cards.add(new SetCardInfo("Zulaport Cutthroat", 190, Rarity.UNCOMMON, mage.cards.z.ZulaportCutthroat.class)); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/OffspringTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/OffspringTest.java index bb7b1fc91786..bb498736758f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/OffspringTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/OffspringTest.java @@ -1,7 +1,12 @@ package org.mage.test.cards.abilities.keywords; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.continuous.GainAbilityControlledSpellsEffect; +import mage.abilities.keyword.OffspringAbility; +import mage.constants.CardType; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.filter.common.FilterNonlandCard; import mage.game.permanent.Permanent; import mage.game.permanent.PermanentToken; import org.junit.Assert; @@ -14,6 +19,8 @@ public class OffspringTest extends CardTestPlayerBase { private static final String vinelasher = "Iridescent Vinelasher"; + private static final String bandit = "Prosperous Bandit"; + private static final String lion = "Silvercoat Lion"; private Permanent getCreature(String name, boolean isToken) { for (Permanent permanent : currentGame.getBattlefield().getActivePermanents(playerA.getId(), currentGame)) { @@ -80,4 +87,151 @@ public void testPay() { checkOffspring(vinelasher, 1, 2, true); } + + @Test + public void testHumilityInResponseNoCopy() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.HAND, playerA, vinelasher); + + addCard(Zone.BATTLEFIELD, playerB, "Plains", 4); + addCard(Zone.BATTLEFIELD, playerB, "Vedalken Orrery"); + addCard(Zone.HAND, playerB, "Humility"); + + setChoice(playerA, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, vinelasher); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Humility", true); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, vinelasher, 1); + assertTokenCount(playerA, vinelasher, 0); + assertPowerToughness(playerA, vinelasher, 1, 1); + } + + @Test + public void testHumilityInResponseNoCopyWithPrintedAndGrantedOffspring() { + addCard(Zone.BATTLEFIELD, playerA, "Zinnia, Valley's Voice"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.HAND, playerA, vinelasher); + + addCard(Zone.BATTLEFIELD, playerB, "Plains", 4); + addCard(Zone.BATTLEFIELD, playerB, "Vedalken Orrery"); + addCard(Zone.HAND, playerB, "Humility"); + + setChoice(playerA, true); + setChoice(playerA, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, vinelasher); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Humility", true); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, vinelasher, 1); + assertTokenCount(playerA, vinelasher, 0); + assertPowerToughness(playerA, vinelasher, 1, 1); + } + + @Test + public void testHumilityInResponseNoCopyWithPrintedAndGrantedOffspringFromNonCreatureSource() { + FilterNonlandCard creatureSpells = new FilterNonlandCard("creature spells"); + creatureSpells.add(CardType.CREATURE.getPredicate()); + addCustomCardWithAbility( + "offspring grant source", + playerA, + new SimpleStaticAbility( + Zone.BATTLEFIELD, + new GainAbilityControlledSpellsEffect(new OffspringAbility("{2}"), creatureSpells) + ), + null, + CardType.ENCHANTMENT, + "", + Zone.BATTLEFIELD + ); + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.HAND, playerA, vinelasher); + + addCard(Zone.BATTLEFIELD, playerB, "Plains", 4); + addCard(Zone.BATTLEFIELD, playerB, "Vedalken Orrery"); + addCard(Zone.HAND, playerB, "Humility"); + + // pay both offspring costs so the granted delayed trigger is definitely created + setChoice(playerA, true); + setChoice(playerA, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, vinelasher); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Humility", true); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, vinelasher, 1); + assertTokenCount(playerA, vinelasher, 0); + assertPowerToughness(playerA, vinelasher, 1, 1); + } + + @Test + public void testGrantedOffspringSourceRemovedBeforeEtbNoCopy() { + addCard(Zone.BATTLEFIELD, playerA, "Zinnia, Valley's Voice"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.HAND, playerA, lion); + addCard(Zone.HAND, playerA, "Path to Exile"); + + setChoice(playerA, true); + setChoice(playerA, false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lion); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Path to Exile", "Zinnia, Valley's Voice"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, lion, 1); + assertTokenCount(playerA, lion, 0); + } + + @Test + public void testPrintedAndGrantedOffspringOnePayment() { + addCard(Zone.BATTLEFIELD, playerA, "Zinnia, Valley's Voice"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 6); + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + addCard(Zone.HAND, playerA, bandit); + + setChoice(playerA, true); + setChoice(playerA, false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bandit); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, bandit, 2); + assertTokenCount(playerA, bandit, 1); + } + + @Test + public void testPrintedAndGrantedOffspringTwoPayments() { + addCard(Zone.BATTLEFIELD, playerA, "Zinnia, Valley's Voice"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 6); + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + addCard(Zone.HAND, playerA, bandit); + + setChoice(playerA, true); + setChoice(playerA, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bandit); + setChoice(playerA, "When this permanent enters"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, bandit, 3); + assertTokenCount(playerA, bandit, 2); + } + } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/blc/ZinniaValleysVoiceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/blc/ZinniaValleysVoiceTest.java new file mode 100644 index 000000000000..5aa4f1cd326a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/blc/ZinniaValleysVoiceTest.java @@ -0,0 +1,76 @@ +package org.mage.test.cards.single.blc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class ZinniaValleysVoiceTest extends CardTestPlayerBase { + + private static final String zinnia = "Zinnia, Valley's Voice"; + private static final String lion = "Silvercoat Lion"; + private static final String bandit = "Prosperous Bandit"; + + @Test + public void testGrantsOffspringToCreatureSpells() { + addCard(Zone.BATTLEFIELD, playerA, zinnia); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); + addCard(Zone.HAND, playerA, lion); + + setChoice(playerA, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lion); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, lion, 2); + assertTokenCount(playerA, lion, 1); + } + + @Test + public void testGrantedOffspringSourceRemovedBeforeEtbNoCopy() { + addCard(Zone.BATTLEFIELD, playerA, zinnia); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.HAND, playerA, lion); + addCard(Zone.HAND, playerA, "Path to Exile"); + + setChoice(playerA, true); + setChoice(playerA, false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lion); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Path to Exile", zinnia); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, lion, 1); + assertTokenCount(playerA, lion, 0); + } + + @Test + public void testRemoveZinniaWhileOffspringTriggersOnStackBothStillResolve() { + addCard(Zone.BATTLEFIELD, playerA, zinnia); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 6); + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + addCard(Zone.HAND, playerA, bandit); + addCard(Zone.BATTLEFIELD, playerB, "Plains"); + addCard(Zone.HAND, playerB, "Path to Exile"); + + setChoice(playerA, true); // Pay printed offspring {1} + setChoice(playerA, true); // Pay granted offspring {2} + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bandit); + setChoice(playerA, "When this permanent enters"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Path to Exile", zinnia, "create a 1/1 token copy of it."); + setChoice(playerA, false); // Decline Path's basic land search + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, zinnia, 0); + assertPermanentCount(playerA, bandit, 3); + assertTokenCount(playerA, bandit, 2); + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/OffspringAbility.java b/Mage/src/main/java/mage/abilities/keyword/OffspringAbility.java index 8be10098297e..b15f09118157 100644 --- a/Mage/src/main/java/mage/abilities/keyword/OffspringAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/OffspringAbility.java @@ -1,38 +1,59 @@ package mage.abilities.keyword; import mage.abilities.Ability; +import mage.abilities.DelayedTriggeredAbility; import mage.abilities.SpellAbility; import mage.abilities.StaticAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; -import mage.abilities.condition.Condition; import mage.abilities.costs.*; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenCopyTargetEffect; +import mage.cards.Card; +import mage.constants.Duration; import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; +import mage.game.events.EntersTheBattlefieldEvent; +import mage.game.events.GameEvent; import mage.game.permanent.Permanent; +import mage.game.stack.Spell; import mage.players.Player; +import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + /** * @author TheElk801 */ public class OffspringAbility extends StaticAbility implements OptionalAdditionalSourceCosts { - + private static final String keywordText = "Offspring"; private static final String reminderText = "You may pay an additional %s as you cast this spell. If you do, when this creature enters, create a 1/1 token copy of it."; private final String rule; + private final OffspringTriggeredAbility offspringTriggeredAbility; public static final String OFFSPRING_ACTIVATION_VALUE_KEY = "offspringActivation"; protected OptionalAdditionalCost additionalCost; - public OffspringAbility(String manaString) { - this(new ManaCostsImpl<>(manaString)); + static String getActivationValueKey(UUID abilityOriginalId) { + return OFFSPRING_ACTIVATION_VALUE_KEY + abilityOriginalId; } + String getActivationValueKey() { + return getActivationValueKey(offspringTriggeredAbility.getOriginalId()); + } + + public OffspringAbility(String manaString) { + this(new ManaCostsImpl<>(manaString)); + } + public OffspringAbility(Cost cost) { super(Zone.STACK, null); this.additionalCost = new OptionalAdditionalCostImpl( @@ -41,90 +62,219 @@ public OffspringAbility(Cost cost) { ); this.additionalCost.setRepeatable(false); this.rule = additionalCost.getName() + ' ' + additionalCost.getReminderText(); + this.offspringTriggeredAbility = new OffspringTriggeredAbility(); + this.addSubAbility(offspringTriggeredAbility); this.setRuleAtTheTop(true); - this.addSubAbility(new EntersBattlefieldTriggeredAbility(new OffspringEffect()) - .withInterveningIf(OffspringCondition.instance).setRuleVisible(false)); } private OffspringAbility(final OffspringAbility ability) { super(ability); this.rule = ability.rule; this.additionalCost = ability.additionalCost.copy(); + this.offspringTriggeredAbility = this.getSubAbilities().stream() + .filter(OffspringTriggeredAbility.class::isInstance) + .map(OffspringTriggeredAbility.class::cast) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Offspring triggered ability wasn't found")); } @Override public OffspringAbility copy() { return new OffspringAbility(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.PutCreatureInPlay, "Pay " + additionalCost.getText(true) + " for offspring?", ability, game)) { - return; - } + } + + @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.PutCreatureInPlay, "Pay " + additionalCost.getText(true) + " for offspring?", ability, game)) { + return; + } additionalCost.activate(); for (Cost cost : ((Costs) additionalCost)) { ability.getCosts().add(cost.copy()); } - ability.setCostsTag(OFFSPRING_ACTIVATION_VALUE_KEY, null); + String activationValueKey = getActivationValueKey(); + ability.setCostsTag(activationValueKey, null); + if (!isIntrinsicOffspringAbility(game, ability)) { + game.addDelayedTriggeredAbility( + new OffspringDelayedTriggeredAbility( + sourceCardHasIntrinsicOffspring(game, ability), + activationValueKey + ), + ability + ); + } } @Override public String getCastMessageSuffix() { return additionalCost.getCastSuffixMessage(0); - } - - @Override + } + + @Override public String getRule() { return rule; } -} + private boolean isIntrinsicOffspringAbility(Game game, Ability spellAbility) { + Card sourceCard = game.getCard(spellAbility.getSourceId()); + return sourceCard != null && sourceCard + .getAbilities() + .stream() + .filter(OffspringAbility.class::isInstance) + .map(Ability::getId) + .anyMatch(this.getId()::equals); + } + + private boolean sourceCardHasIntrinsicOffspring(Game game, Ability spellAbility) { + Card sourceCard = game.getCard(spellAbility.getSourceId()); + return sourceCard != null + && sourceCard.getAbilities().stream().anyMatch(OffspringAbility.class::isInstance); + } + + public static void syncActivationTagsWithCurrentSpellAbilities(Spell spell, Game game) { + SpellAbility spellAbility = spell.getSpellAbility(); + Map costsTagMap = spellAbility.getCostsTagMap(); + if (costsTagMap == null || costsTagMap.isEmpty()) { + return; + } + + Set activeOffspringActivationKeys = spell + .getAbilities(game) + .stream() + .filter(OffspringAbility.class::isInstance) + .map(OffspringAbility.class::cast) + .map(OffspringAbility::getActivationValueKey) + .collect(Collectors.toSet()); + + costsTagMap.keySet().removeIf(tag -> + tag.startsWith(OFFSPRING_ACTIVATION_VALUE_KEY) + && !activeOffspringActivationKeys.contains(tag) + ); + } +} + class OffspringEffect extends OneShotEffect { OffspringEffect() { super(Outcome.Benefit); staticText = "create a 1/1 token copy of it"; + } + + private OffspringEffect(final OffspringEffect effect) { + super(effect); + } + + @Override + public OffspringEffect copy() { + return new OffspringEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = null; + List pointerTargets = getTargetPointer().getTargets(game, source); + if (pointerTargets != null && !pointerTargets.isEmpty()) { + permanent = getTargetPointer().getFirstTargetPermanentOrLKI(game, source); + } + if (permanent == null) { + permanent = source.getSourcePermanentOrLKI(game); + } + return permanent != null && new CreateTokenCopyTargetEffect( + null, null, false, 1, false, + false, null, 1, 1, false + ).setSavedPermanent(permanent).apply(game, source); } +} + +class OffspringTriggeredAbility extends EntersBattlefieldTriggeredAbility { - private OffspringEffect(final OffspringEffect effect) { - super(effect); + OffspringTriggeredAbility() { + super(new OffspringEffect(), false); + setTriggerPhrase("When this permanent enters, "); + this.setRuleVisible(false); + } + + private OffspringTriggeredAbility(final OffspringTriggeredAbility ability) { + super(ability); } @Override - public OffspringEffect copy() { - return new OffspringEffect(this); + public OffspringTriggeredAbility copy() { + return new OffspringTriggeredAbility(this); } @Override - public boolean apply(Game game, Ability source) { - Permanent permanent = source.getSourcePermanentOrLKI(game); - return permanent != null && new CreateTokenCopyTargetEffect( - null, null, false, 1, false, - false, null, 1, 1, false - ).setSavedPermanent(permanent).apply(game, source); + public boolean checkTrigger(GameEvent event, Game game) { + if (!super.checkTrigger(event, game)) { + return false; + } + return CardUtil.checkSourceCostsTagExists( + game, + this, + OffspringAbility.getActivationValueKey(this.getOriginalId()) + ); } } -enum OffspringCondition implements Condition { - instance; +class OffspringDelayedTriggeredAbility extends DelayedTriggeredAbility { + + private final boolean skipIfNoAbilitiesOnEtb; + private final String activationValueKey; + + OffspringDelayedTriggeredAbility(boolean skipIfNoAbilitiesOnEtb, String activationValueKey) { + super(new OffspringEffect(), Duration.EndOfTurn, true); + this.skipIfNoAbilitiesOnEtb = skipIfNoAbilitiesOnEtb; + this.activationValueKey = activationValueKey; + this.setRuleVisible(false); + } + + private OffspringDelayedTriggeredAbility(final OffspringDelayedTriggeredAbility ability) { + super(ability); + this.skipIfNoAbilitiesOnEtb = ability.skipIfNoAbilitiesOnEtb; + this.activationValueKey = ability.activationValueKey; + } @Override - public boolean apply(Game game, Ability source) { - return CardUtil.checkSourceCostsTagExists(game, source, OffspringAbility.OFFSPRING_ACTIVATION_VALUE_KEY); + public OffspringDelayedTriggeredAbility copy() { + return new OffspringDelayedTriggeredAbility(this); } @Override - public String toString() { - return "its offspring cost was paid"; + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!event.getTargetId().equals(getSourceId())) { + return false; + } + if (!(event instanceof EntersTheBattlefieldEvent)) { + return false; + } + Permanent permanent = ((EntersTheBattlefieldEvent) event).getTarget(); + if (permanent == null + || permanent.getZoneChangeCounter(game) != getStackMomentSourceZCC() + 1) { + return false; + } + if (!CardUtil.checkSourceCostsTagExists(game, this, activationValueKey)) { + return false; + } + if (skipIfNoAbilitiesOnEtb && permanent.getAbilities().isEmpty()) { + // Printed offspring was stripped from the entering creature (e.g. by Humility), + // so the granted offspring fallback must also not trigger. + return false; + } + getEffects().setTargetPointer(new FixedTarget(permanent, game)); + return true; } } diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index f79d89a8f66e..7cbe50dc5505 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -7,6 +7,7 @@ import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.keyword.BestowAbility; +import mage.abilities.keyword.OffspringAbility; import mage.abilities.keyword.PrototypeAbility; import mage.cards.*; import mage.constants.*; @@ -356,6 +357,7 @@ public boolean resolve(Game game) { } } else { permId = card.getId(); + OffspringAbility.syncActivationTagsWithCurrentSpellAbilities(this, game); MageObjectReference mor = new MageObjectReference(getSpellAbility()); game.storePermanentCostsTags(mor, getSpellAbility()); permanentCreated = controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null); @@ -389,6 +391,7 @@ public boolean resolve(Game game) { } // Aura has no legal target and its a bestow enchantment -> Add it to battlefield as creature if (bestow) { + OffspringAbility.syncActivationTagsWithCurrentSpellAbilities(this, game); MageObjectReference mor = new MageObjectReference(getSpellAbility()); game.storePermanentCostsTags(mor, getSpellAbility()); return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null); @@ -406,6 +409,7 @@ public boolean resolve(Game game) { token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, null, false); return true; } else { + OffspringAbility.syncActivationTagsWithCurrentSpellAbilities(this, game); MageObjectReference mor = new MageObjectReference(getSpellAbility()); game.storePermanentCostsTags(mor, getSpellAbility()); return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);