From a37fc0589a4b97b4b30be07af9878a3821357be3 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sat, 21 Oct 2023 15:26:38 +0200 Subject: [PATCH] [LCI] Implement The Skullspore Nexus (#11327) --- Mage.Sets/src/mage/cards/t/TheGreatHenge.java | 57 ++---- .../src/mage/cards/t/TheSkullsporeNexus.java | 167 ++++++++++++++++++ .../src/mage/sets/LostCavernsOfIxalan.java | 1 + .../single/lci/TheSkullsporeNexusTest.java | 68 +++++++ .../game/events/ZoneChangeGroupEvent.java | 11 +- .../permanent/token/FungusDinosaurToken.java | 29 +++ 6 files changed, 283 insertions(+), 50 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/t/TheSkullsporeNexus.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/lci/TheSkullsporeNexusTest.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/FungusDinosaurToken.java diff --git a/Mage.Sets/src/mage/cards/t/TheGreatHenge.java b/Mage.Sets/src/mage/cards/t/TheGreatHenge.java index a46ed29443c..54a11b8030a 100644 --- a/Mage.Sets/src/mage/cards/t/TheGreatHenge.java +++ b/Mage.Sets/src/mage/cards/t/TheGreatHenge.java @@ -1,25 +1,24 @@ package mage.cards.t; -import mage.MageInt; import mage.Mana; import mage.abilities.Ability; -import mage.abilities.SpellAbility; import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.dynamicvalue.common.GreatestPowerAmongControlledCreaturesValue; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.GainLifeEffect; -import mage.abilities.effects.common.cost.CostModificationEffectImpl; +import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; import mage.abilities.mana.SimpleManaAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.SetTargetPointer; +import mage.constants.SuperType; +import mage.constants.Zone; import mage.counters.CounterType; import mage.filter.StaticFilters; -import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.util.CardUtil; import java.util.UUID; @@ -34,7 +33,9 @@ public final class TheGreatHenge extends CardImpl { this.supertype.add(SuperType.LEGENDARY); // This spell costs {X} less to cast, where X is the greatest power among creatures you control. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new TheGreatHengeCostReductionEffect())); + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new SpellCostReductionSourceEffect(GreatestPowerAmongControlledCreaturesValue.instance) + ).setRuleAtTheTop(true).addHint(GreatestPowerAmongControlledCreaturesValue.getHint())); // {T}: Add {G}{G}. You gain 2 life. Ability ability = new SimpleManaAbility(Zone.BATTLEFIELD, Mana.GreenMana(2), new TapSourceCost()); @@ -59,42 +60,4 @@ public final class TheGreatHenge extends CardImpl { public TheGreatHenge copy() { return new TheGreatHenge(this); } -} - -class TheGreatHengeCostReductionEffect extends CostModificationEffectImpl { - - TheGreatHengeCostReductionEffect() { - super(Duration.WhileOnStack, Outcome.Benefit, CostModificationType.REDUCE_COST); - staticText = "This spell costs {X} less to cast, where X is the greatest power among creatures you control"; - } - - private TheGreatHengeCostReductionEffect(final TheGreatHengeCostReductionEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source, Ability abilityToModify) { - int reductionAmount = game.getBattlefield() - .getAllActivePermanents( - StaticFilters.FILTER_PERMANENT_CREATURE, abilityToModify.getControllerId(), game - ).stream() - .map(Permanent::getPower) - .mapToInt(MageInt::getValue) - .max() - .orElse(0); - CardUtil.reduceCost(abilityToModify, Math.max(0, reductionAmount)); - return true; - } - - @Override - public boolean applies(Ability abilityToModify, Ability source, Game game) { - return abilityToModify instanceof SpellAbility - && abilityToModify.getSourceId().equals(source.getSourceId()) - && game.getCard(abilityToModify.getSourceId()) != null; - } - - @Override - public TheGreatHengeCostReductionEffect copy() { - return new TheGreatHengeCostReductionEffect(this); - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/t/TheSkullsporeNexus.java b/Mage.Sets/src/mage/cards/t/TheSkullsporeNexus.java new file mode 100644 index 00000000000..26ff5ef4cd9 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheSkullsporeNexus.java @@ -0,0 +1,167 @@ +package mage.cards.t; + +import mage.MageItem; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.dynamicvalue.common.GreatestPowerAmongControlledCreaturesValue; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeGroupEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.FungusDinosaurToken; +import mage.target.common.TargetCreaturePermanent; +import mage.target.targetpointer.FixedTarget; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author Susucr + */ +public final class TheSkullsporeNexus extends CardImpl { + + public TheSkullsporeNexus(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{6}{G}{G}"); + + this.supertype.add(SuperType.LEGENDARY); + + // This spell costs {X} less to cast, where X is the greatest power among creatures you control. + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new SpellCostReductionSourceEffect(GreatestPowerAmongControlledCreaturesValue.instance) + ).setRuleAtTheTop(true).addHint(GreatestPowerAmongControlledCreaturesValue.getHint())); + + // Whenever one or more nontoken creatures you control die, create a green Fungus Dinosaur creature token with base power and toughness each equal to the total power of those creatures. + this.addAbility(new TheSkullsporeNexusTrigger()); + + // {2}, {T}: Double target creature's power until end of turn. + Ability ability = new SimpleActivatedAbility( + new TheSkullsporeNexusDoubleEffect(), + new GenericManaCost(2) + ); + ability.addCost(new TapSourceCost()); + ability.addTarget(new TargetCreaturePermanent()); + this.addAbility(ability); + } + + private TheSkullsporeNexus(final TheSkullsporeNexus card) { + super(card); + } + + @Override + public TheSkullsporeNexus copy() { + return new TheSkullsporeNexus(this); + } +} + +class TheSkullsporeNexusTrigger extends TriggeredAbilityImpl { + + TheSkullsporeNexusTrigger() { + super(Zone.BATTLEFIELD, null, false); + } + + private TheSkullsporeNexusTrigger(final TheSkullsporeNexusTrigger ability) { + super(ability); + } + + @Override + public TheSkullsporeNexusTrigger copy() { + return new TheSkullsporeNexusTrigger(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE_GROUP; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + ZoneChangeGroupEvent zEvent = (ZoneChangeGroupEvent) event; + if (!zEvent.isDiesEvent()) { + return false; + } + + List permanents = zEvent.getCards().stream() + .map(MageItem::getId) + .map(game::getPermanentOrLKIBattlefield) + .filter(Objects::nonNull) + .filter(p -> p.isCreature(game) && p.isControlledBy(getControllerId())) + .collect(Collectors.toList()); + + if (permanents.isEmpty()) { + return false; + } + + int amount = permanents.stream().mapToInt(p -> p.getPower().getValue()).sum(); + + this.getEffects().clear(); + Effect effect = new CreateTokenEffect(new FungusDinosaurToken(amount)); + effect.setValue(infoKey, amount); + this.getEffects().add(effect); + return true; + } + + private static final String infoKey = "totalpower"; + + @Override + public String getRule() { + // this trigger might want to expose extra info on the stack + String triggeredInfo = ""; + if (!this.getEffects().isEmpty()) { + triggeredInfo += "

Total power: " + this.getEffects().get(0).getValue(infoKey) + ""; + } + return "Whenever one or more nontoken creatures you control die, " + + "create a green Fungus Dinosaur creature token with base power and toughness " + + "each equal to the total power of those creatures." + triggeredInfo; + } + + @Override + public boolean isInUseableZone(Game game, MageObject source, GameEvent event) { + return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, event, game); + } +} + +class TheSkullsporeNexusDoubleEffect extends OneShotEffect { + + TheSkullsporeNexusDoubleEffect() { + super(Outcome.Benefit); + staticText = "double target creature's power until end of turn"; + } + + private TheSkullsporeNexusDoubleEffect(final TheSkullsporeNexusDoubleEffect effect) { + super(effect); + } + + @Override + public TheSkullsporeNexusDoubleEffect copy() { + return new TheSkullsporeNexusDoubleEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (permanent == null) { + return false; + } + + ContinuousEffect boost = new BoostTargetEffect(permanent.getPower().getValue(), 0, Duration.EndOfTurn) + .setTargetPointer(new FixedTarget(permanent, game)); + game.addEffect(boost, source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/LostCavernsOfIxalan.java b/Mage.Sets/src/mage/sets/LostCavernsOfIxalan.java index e2d90bfa7a3..a4a29964908 100644 --- a/Mage.Sets/src/mage/sets/LostCavernsOfIxalan.java +++ b/Mage.Sets/src/mage/sets/LostCavernsOfIxalan.java @@ -30,5 +30,6 @@ public final class LostCavernsOfIxalan extends ExpansionSet { cards.add(new SetCardInfo("Plains", 287, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Swamp", 289, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Temple of Power", 317, Rarity.MYTHIC, mage.cards.t.TempleOfPower.class)); + cards.add(new SetCardInfo("The Skullspore Nexus", 212, Rarity.MYTHIC, mage.cards.t.TheSkullsporeNexus.class)); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/TheSkullsporeNexusTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/TheSkullsporeNexusTest.java new file mode 100644 index 00000000000..19652fd7404 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/TheSkullsporeNexusTest.java @@ -0,0 +1,68 @@ +package org.mage.test.cards.single.lci; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class TheSkullsporeNexusTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.t.TheSkullsporeNexus}
+ * The Skullspore Nexus {6}{G}{G}
+ * Legendary Artifact
+ * This spell costs {X} less to cast, where X is the greatest power among creatures you control.
+ * Whenever one or more nontoken creatures you control die, create a green Fungus Dinosaur creature token with base power and toughness each equal to the total power of those creatures.
+ * {2}, {T}: Double target creature’s power until end of turn.
+ */ + private static final String nexus = "The Skullspore Nexus"; + + @Test + public void test_trigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, nexus); + addCard(Zone.BATTLEFIELD, playerA, "Butcher Ghoul"); // 1/1 undying + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); // 2/2 doesn't count as not controlled + addCard(Zone.HAND, playerA, "Grave Titan"); // 6/6, etb with 2 2/2 tokens + addCard(Zone.HAND, playerA, "Damnation", 2); // destroy all creatures. + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 6 + 4 * 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grave Titan", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Damnation", true); + setChoice(playerA, "Whenever one or more nontoken creatures you control die"); // ordering triggers + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + // Only 2 creatures do count: 6 from Titan + 1 from Ghoul + checkPT("after first damnation", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Fungus Dinosaur Token", 7, 7); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Damnation", true); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + // only undying ghoul counts (for 2) + assertPowerToughness(playerA, "Fungus Dinosaur Token", 2, 2); + } + + @Test + public void test_activation() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, nexus); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); // 2/2 + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, {T}: Double", "Grizzly Bears"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPowerToughness(playerA, "Grizzly Bears", 4, 2); + assertTappedCount("Swamp", true, 2); + assertTapped(nexus, true); + } +} diff --git a/Mage/src/main/java/mage/game/events/ZoneChangeGroupEvent.java b/Mage/src/main/java/mage/game/events/ZoneChangeGroupEvent.java index 129b4167cf0..4477fd7d207 100644 --- a/Mage/src/main/java/mage/game/events/ZoneChangeGroupEvent.java +++ b/Mage/src/main/java/mage/game/events/ZoneChangeGroupEvent.java @@ -1,11 +1,12 @@ package mage.game.events; +import mage.abilities.Ability; import mage.cards.Card; import mage.constants.Zone; import mage.game.permanent.PermanentToken; + import java.util.Set; import java.util.UUID; -import mage.abilities.Ability; /** * @author LevelX2 @@ -18,7 +19,7 @@ public class ZoneChangeGroupEvent extends GameEvent { private final Set tokens; /* added this */ Ability source; - public ZoneChangeGroupEvent(Set cards, Set tokens, UUID sourceId, Ability source, UUID playerId, Zone fromZone, Zone toZone) { + public ZoneChangeGroupEvent(Set cards, Set tokens, UUID sourceId, Ability source, UUID playerId, Zone fromZone, Zone toZone) { super(GameEvent.EventType.ZONE_CHANGE_GROUP, null, null, playerId); this.fromZone = fromZone; this.toZone = toZone; @@ -35,6 +36,10 @@ public class ZoneChangeGroupEvent extends GameEvent { return toZone; } + public boolean isDiesEvent() { + return (toZone == Zone.GRAVEYARD && fromZone == Zone.BATTLEFIELD); + } + public Set getCards() { return cards; } @@ -42,7 +47,7 @@ public class ZoneChangeGroupEvent extends GameEvent { public Set getTokens() { return tokens; } - + public Ability getSource() { return source; } diff --git a/Mage/src/main/java/mage/game/permanent/token/FungusDinosaurToken.java b/Mage/src/main/java/mage/game/permanent/token/FungusDinosaurToken.java new file mode 100644 index 00000000000..1b81ec8184f --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/FungusDinosaurToken.java @@ -0,0 +1,29 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.constants.CardType; +import mage.constants.SubType; + +/** + * @author Susucr + */ +public final class FungusDinosaurToken extends TokenImpl { + + public FungusDinosaurToken(int xValue) { + super("Fungus Dinosaur Token", "X/X green Fungus Dinosaur creature token"); + cardType.add(CardType.CREATURE); + subtype.add(SubType.FUNGUS); + subtype.add(SubType.DINOSAUR); + color.setGreen(true); + power = new MageInt(xValue); + toughness = new MageInt(xValue); + } + + private FungusDinosaurToken(final FungusDinosaurToken token) { + super(token); + } + + public FungusDinosaurToken copy() { + return new FungusDinosaurToken(this); + } +}