Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions Mage.Sets/src/mage/cards/z/ZinniaValleysVoice.java
Original file line number Diff line number Diff line change
@@ -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);
}
}

1 change: 1 addition & 0 deletions Mage.Sets/src/mage/sets/BloomburrowCommander.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,6 +19,33 @@
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 static final String genericGrantSource = "Generic Offspring Grant Source";

// CR 702.175a/b: Offspring is an additional cost and creates a linked ETB trigger; multiple instances are paid separately.
// CR 607.2i, 607.5: linked abilities remain linked per instance, including abilities gained from other effects.
// Keep these mechanic regressions generic; actual Zinnia coverage stays in ZinniaValleysVoiceTest.

private void addGenericOffspringGrantSource(String name) {
// Zinnia is currently the only printed card in the test card pool that grants Offspring to creature spells.
// This helper isolates the generic granted-Offspring engine path and lets these tests cover multiple
// independent grant sources without mixing in Zinnia-specific copy/legend/creature behavior.
FilterNonlandCard creatureSpells = new FilterNonlandCard("creature spells");
creatureSpells.add(CardType.CREATURE.getPredicate());
addCustomCardWithAbility(
name,
playerA,
new SimpleStaticAbility(
Zone.BATTLEFIELD,
new GainAbilityControlledSpellsEffect(new OffspringAbility("{2}"), creatureSpells)
),
null,
CardType.ENCHANTMENT,
"",
Zone.BATTLEFIELD
);
}

private Permanent getCreature(String name, boolean isToken) {
for (Permanent permanent : currentGame.getBattlefield().getActivePermanents(playerA.getId(), currentGame)) {
Expand Down Expand Up @@ -80,4 +112,209 @@ 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() {
// Use a generic noncreature grant source so this stays a mechanic regression, not a Zinnia card test.
addGenericOffspringGrantSource(genericGrantSource);
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 testTwoGrantedOffspringAbilitiesOnePayment() {
addGenericOffspringGrantSource("Generic Offspring Grant Source A");
addGenericOffspringGrantSource("Generic Offspring Grant Source B");

addCard(Zone.BATTLEFIELD, playerA, "Plains", 6);
addCard(Zone.HAND, playerA, lion);

setChoice(playerA, true);
setChoice(playerA, false);
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() {
addGenericOffspringGrantSource(genericGrantSource);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 6);
addCard(Zone.HAND, playerA, lion);
addCard(Zone.HAND, playerA, "Disenchant");

setChoice(playerA, true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lion);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Disenchant", genericGrantSource);

setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();

assertPermanentCount(playerA, lion, 1);
assertTokenCount(playerA, lion, 0);
}

@Test
public void testPrintedAndGrantedOffspringOnePayment() {
addGenericOffspringGrantSource(genericGrantSource);
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() {
addGenericOffspringGrantSource(genericGrantSource);
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, bandit); // stack both offspring triggers (2 triggers -> 1 choice)

setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();

assertPermanentCount(playerA, bandit, 3);
assertTokenCount(playerA, bandit, 2);
}

@Test
public void testCopyingSpellMustKeepOffspringStatus() {
addCard(Zone.HAND, playerA, bandit, 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.HAND, playerA, "Double Major", 1);

activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bandit);
setChoice(playerA, true); // pay offspring once

castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", bandit, bandit, StackClause.WHILE_ON_STACK);
checkStackSize("before copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // bandit + double major
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
checkStackSize("after copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // spell + copy

setStrictChooseMode(false);
setStopAt(1, PhaseStep.END_TURN);
execute();

assertPermanentCount(playerA, bandit, 4);
assertTokenCount(playerA, bandit, 3);
}

@Test
public void testCopyingEtbTriggerMustKeepOffspringStatus() {
addCard(Zone.HAND, playerA, bandit, 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
addCard(Zone.BATTLEFIELD, playerA, "Strionic Resonator", 1);

castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bandit);
setChoice(playerA, true); // pay offspring once
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);

// bandit resolved, its offspring trigger is on the stack; copy that trigger
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA,
"{2}, {T}: Copy target triggered ability you control. You may choose new targets for the copy.");

setStrictChooseMode(false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();

assertPermanentCount(playerA, bandit, 3);
assertTokenCount(playerA, bandit, 2);
}

@Test
public void testPrintedAndGrantedOffspringRollbackClearsOldPayments() {
addGenericOffspringGrantSource(genericGrantSource);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
addCard(Zone.HAND, playerA, bandit);

// first line: pay both offspring costs, then roll back to the start of the turn
setChoice(playerA, true);
setChoice(playerA, true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bandit);
setChoice(playerA, bandit);

rollbackTurns(1, PhaseStep.BEGIN_COMBAT, playerA, 0);
rollbackAfterActionsStart();

// after rollback, only the printed offspring payment should be remembered
setChoice(playerA, true);
setChoice(playerA, false);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bandit);

rollbackAfterActionsEnd();

setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();

assertPermanentCount(playerA, bandit, 2);
assertTokenCount(playerA, bandit, 1);
}

}
Loading
Loading