From 204f67c5f0eec72a5ec01c69a1cdf597a1ff2c36 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Fri, 12 Jul 2024 23:38:29 -0400 Subject: [PATCH] Implement offspring mechanic (#12572) * implement offspring mechanic * create offspring test --- Mage.Sets/src/mage/sets/Bloomburrow.java | 6 - .../abilities/keywords/OffspringTest.java | 83 ++++++++++++++ .../abilities/keyword/OffspringAbility.java | 107 +++++++++++++++++- 3 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/OffspringTest.java diff --git a/Mage.Sets/src/mage/sets/Bloomburrow.java b/Mage.Sets/src/mage/sets/Bloomburrow.java index 4270195f677..c172431eca8 100644 --- a/Mage.Sets/src/mage/sets/Bloomburrow.java +++ b/Mage.Sets/src/mage/sets/Bloomburrow.java @@ -4,15 +4,11 @@ import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; -import java.util.Arrays; -import java.util.List; - /** * @author TheElk801 */ public final class Bloomburrow extends ExpansionSet { - private static final List unfinished = Arrays.asList("Coruscation Mage", "Darkstar Augur", "Finch Formation", "Flowerfoot Swordmaster", "Iridescent Vinelasher", "Manifold Mouse", "Splash Lasher", "Steampath Charger", "Tender Wildguide", "Thundertrap Trainer", "Warren Warleader"); private static final Bloomburrow instance = new Bloomburrow(); public static Bloomburrow getInstance() { @@ -120,7 +116,5 @@ public final class Bloomburrow extends ExpansionSet { cards.add(new SetCardInfo("Wandertale Mentor", 240, Rarity.UNCOMMON, mage.cards.w.WandertaleMentor.class)); cards.add(new SetCardInfo("Warren Warleader", 38, Rarity.MYTHIC, mage.cards.w.WarrenWarleader.class)); cards.add(new SetCardInfo("Zoraline, Cosmos Caller", 242, Rarity.RARE, mage.cards.z.ZoralineCosmosCaller.class)); - - cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); // remove when mechanic is implemented } } 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 new file mode 100644 index 00000000000..bb7b1fc9178 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/OffspringTest.java @@ -0,0 +1,83 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentToken; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class OffspringTest extends CardTestPlayerBase { + + private static final String vinelasher = "Iridescent Vinelasher"; + + private Permanent getCreature(String name, boolean isToken) { + for (Permanent permanent : currentGame.getBattlefield().getActivePermanents(playerA.getId(), currentGame)) { + if (name.equals(permanent.getName()) && (permanent instanceof PermanentToken) == isToken) { + return permanent; + } + } + return null; + } + + private void checkOffspring(String name, int power, int toughness, boolean paid) { + assertPermanentCount(playerA, name, paid ? 2 : 1); + assertTokenCount(playerA, name, paid ? 1 : 0); + + Permanent original = getCreature(name, false); + Assert.assertEquals( + "Original creature should have power " + power, + power, original.getPower().getValue() + ); + Assert.assertEquals( + "Original creature should have toughness " + toughness, + toughness, original.getToughness().getValue() + ); + if (!paid) { + return; + } + Permanent token = getCreature(name, true); + Assert.assertEquals( + "Token creature should have power 1", + 1, token.getPower().getValue() + ); + Assert.assertEquals( + "Token creature should have toughness 1", + 1, token.getToughness().getValue() + ); + } + + @Test + public void testNoPay() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.HAND, playerA, vinelasher); + + setChoice(playerA, false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, vinelasher); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + checkOffspring(vinelasher, 1, 2, false); + } + + @Test + public void testPay() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.HAND, playerA, vinelasher); + + setChoice(playerA, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, vinelasher); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + checkOffspring(vinelasher, 1, 2, true); + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/OffspringAbility.java b/Mage/src/main/java/mage/abilities/keyword/OffspringAbility.java index 057026f73ec..9888c995dde 100644 --- a/Mage/src/main/java/mage/abilities/keyword/OffspringAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/OffspringAbility.java @@ -1,26 +1,58 @@ package mage.abilities.keyword; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; import mage.abilities.StaticAbility; -import mage.abilities.costs.Cost; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.*; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenCopyTargetEffect; +import mage.constants.Outcome; import mage.constants.Zone; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.util.CardUtil; /** * @author TheElk801 - * TODO: Implement this */ -public class OffspringAbility extends StaticAbility { +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; + + public static final String OFFSPRING_ACTIVATION_VALUE_KEY = "offspringActivation"; + + protected OptionalAdditionalCost additionalCost; public OffspringAbility(String manaString) { this(new ManaCostsImpl<>(manaString)); } public OffspringAbility(Cost cost) { - super(Zone.ALL, null); + super(Zone.STACK, null); + this.additionalCost = new OptionalAdditionalCostImpl( + keywordText + ' ' + cost.getText(), + String.format(reminderText, cost.getText()), cost + ); + this.additionalCost.setRepeatable(false); + this.rule = additionalCost.getName() + ' ' + additionalCost.getReminderText(); + this.setRuleAtTheTop(true); + this.addSubAbility(new ConditionalInterveningIfTriggeredAbility( + new EntersBattlefieldTriggeredAbility(new OffspringEffect()), OffspringCondition.instance, + "When this creature enters, if its offspring cost was paid, create a 1/1 token copy of it." + ).setRuleVisible(false)); } private OffspringAbility(final OffspringAbility ability) { super(ability); + this.rule = ability.rule; + this.additionalCost = ability.additionalCost.copy(); } @Override @@ -28,8 +60,73 @@ public class OffspringAbility extends StaticAbility { 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; + } + additionalCost.activate(); + for (Cost cost : ((Costs) additionalCost)) { + ability.getCosts().add(cost.copy()); + } + ability.setCostsTag(OFFSPRING_ACTIVATION_VALUE_KEY, null); + } + + @Override + public String getCastMessageSuffix() { + return additionalCost.getCastSuffixMessage(0); + } + @Override public String getRule() { - return "Offspring"; + return rule; + } +} + +class OffspringEffect extends OneShotEffect { + + OffspringEffect() { + super(Outcome.Benefit); + } + + 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 = source.getSourcePermanentOrLKI(game); + return permanent != null && new CreateTokenCopyTargetEffect( + null, null, false, 1, false, + false, null, 1, 1, false + ).setSavedPermanent(permanent).apply(game, source); + } +} + +enum OffspringCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + return CardUtil.checkSourceCostsTagExists(game, source, OffspringAbility.OFFSPRING_ACTIVATION_VALUE_KEY); + } + + @Override + public String toString() { + return "Offspring cost was paid"; } }