From bc2ba3e4ebd7c9ece81ce0c1ba0d319e01d50915 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sat, 3 May 2025 17:37:50 +0200 Subject: [PATCH] Implement [40K] The Red Terror --- Mage.Sets/src/mage/cards/t/TheRedTerror.java | 93 +++++++ .../mage/sets/Warhammer40000Commander.java | 1 + .../cards/single/_40k/TheRedTerrorTest.java | 254 ++++++++++++++++++ .../DealsDamageToAnyTriggeredAbility.java | 2 + 4 files changed, 350 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TheRedTerror.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/_40k/TheRedTerrorTest.java diff --git a/Mage.Sets/src/mage/cards/t/TheRedTerror.java b/Mage.Sets/src/mage/cards/t/TheRedTerror.java new file mode 100644 index 00000000000..96960a7b93b --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheRedTerror.java @@ -0,0 +1,93 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.BatchTriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.Controllable; +import mage.game.Game; +import mage.game.events.DamagedBatchBySourceEvent; +import mage.game.events.DamagedEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class TheRedTerror extends CardImpl { + + public TheRedTerror(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.TYRANID); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // Advanced Species -- Whenever a red source you control deals damage to one or more permanents and/or players, put a +1/+1 counter on The Red Terror. + this.addAbility(new TheRedTerrorTrigger().withFlavorWord("Advanced Species")); + } + + private TheRedTerror(final TheRedTerror card) { + super(card); + } + + @Override + public TheRedTerror copy() { + return new TheRedTerror(this); + } +} + +class TheRedTerrorTrigger extends TriggeredAbilityImpl implements BatchTriggeredAbility { + + public TheRedTerrorTrigger() { + super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.P1P1.createInstance(1))); + setTriggerPhrase("Whenever a red source you control deals damage to one or more permanents and/or players"); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_BATCH_BY_SOURCE; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + DamagedBatchBySourceEvent batchBySourceEvent = (DamagedBatchBySourceEvent) event; + + UUID sourceController = null; + MageObject sourceObject; + Permanent sourcePermanent = game.getPermanentOrLKIBattlefield(batchBySourceEvent.getSourceId()); + if (sourcePermanent == null) { + sourceObject = game.getObject(event.getSourceId()); + if (sourceObject instanceof Controllable) { + sourceController = ((Controllable) sourceObject).getControllerId(); + } + } else { + sourceObject = sourcePermanent; + sourceController = sourcePermanent.getControllerId(); + } + + return sourceObject.getColor(game).isRed() // a red source + && getControllerId().equals(sourceController) // you control + && batchBySourceEvent.getAmount() > 0; // deals damage (skipping the permanent and/or player check, they are the only damageable) + } + + private TheRedTerrorTrigger(final TheRedTerrorTrigger trigger) { + super(trigger); + } + + @Override + public TheRedTerrorTrigger copy() { + return new TheRedTerrorTrigger(this); + } +} diff --git a/Mage.Sets/src/mage/sets/Warhammer40000Commander.java b/Mage.Sets/src/mage/sets/Warhammer40000Commander.java index 5b326c80179..f358ee06a0a 100644 --- a/Mage.Sets/src/mage/sets/Warhammer40000Commander.java +++ b/Mage.Sets/src/mage/sets/Warhammer40000Commander.java @@ -266,6 +266,7 @@ public final class Warhammer40000Commander extends ExpansionSet { cards.add(new SetCardInfo("The Golden Throne", 157, Rarity.RARE, mage.cards.t.TheGoldenThrone.class)); cards.add(new SetCardInfo("The Horus Heresy", 126, Rarity.RARE, mage.cards.t.TheHorusHeresy.class)); cards.add(new SetCardInfo("The Lost and the Damned", 129, Rarity.UNCOMMON, mage.cards.t.TheLostAndTheDamned.class)); + cards.add(new SetCardInfo("The Red Terror", 83, Rarity.RARE, mage.cards.t.TheRedTerror.class)); cards.add(new SetCardInfo("The Ruinous Powers", 139, Rarity.RARE, mage.cards.t.TheRuinousPowers.class)); cards.add(new SetCardInfo("The Swarmlord", 4, Rarity.MYTHIC, mage.cards.t.TheSwarmlord.class)); cards.add(new SetCardInfo("The War in Heaven", 69, Rarity.RARE, mage.cards.t.TheWarInHeaven.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/_40k/TheRedTerrorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/_40k/TheRedTerrorTest.java new file mode 100644 index 00000000000..01e2cac5cea --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/_40k/TheRedTerrorTest.java @@ -0,0 +1,254 @@ +package org.mage.test.cards.single._40k; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class TheRedTerrorTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.t.TheRedTerror The Red Terror} {3}{R} + * Legendary Creature — Tyranid + * Advanced Species — Whenever a red source you control deals damage to one or more permanents and/or players, put a +1/+1 counter on The Red Terror. + * 4/3 + */ + private static final String terror = "The Red Terror"; + + /** + * 2/1, notably non-red + */ + private static final String vanguard = "Elite Vanguard"; + /** + * 2/1, notably red + */ + private static final String piker = "Goblin Piker"; + /** + * 2/1, notably red + */ + private static final String earthElemental = "Earth Elemental"; + + /** + * {R} 3 damage to any target + */ + private static final String bolt = "Lightning Bolt"; + /** + * Rivals' Duel {3}{R} + * Sorcery + * Choose two target creatures that share no creature types. Those creatures fight each other. + */ + private static final String duel = "Rivals' Duel"; + /** + * {1}{R} 2 damage to each creature + */ + private static final String pyroclasm = "Pyroclasm"; + /** + * Fiery Confluence {2}{R}{R} + * Sorcery + * Choose three. You may choose the same mode more than once. + * • Fiery Confluence deals 1 damage to each creature. + * • Fiery Confluence deals 2 damage to each opponent. + * • Destroy target artifact. + */ + private static final String confluence = "Fiery Confluence"; + + @Test + public void testTriggerCombatSimple() { + addCard(Zone.BATTLEFIELD, playerA, terror, 1); + + attack(1, playerA, terror, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount(playerA, terror, CounterType.P1P1, 1); + assertLife(playerB, 20 - 4); + } + + @Test + public void testNoTriggerCombatNonRed() { + addCard(Zone.BATTLEFIELD, playerA, terror, 1); + addCard(Zone.BATTLEFIELD, playerA, vanguard, 1); + + attack(1, playerA, vanguard, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount(playerA, terror, CounterType.P1P1, 0); + assertLife(playerB, 20 - 2); + } + + @Test + public void testTriggerCombatRed() { + addCard(Zone.BATTLEFIELD, playerA, terror, 1); + addCard(Zone.BATTLEFIELD, playerA, earthElemental, 1); + + attack(1, playerA, earthElemental, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount(playerA, terror, CounterType.P1P1, 1); + assertLife(playerB, 20 - 4); + } + + @Test + public void testDoubleTriggerCombatRed() { + addCard(Zone.BATTLEFIELD, playerA, terror, 1); + addCard(Zone.BATTLEFIELD, playerA, vanguard, 1); + addCard(Zone.BATTLEFIELD, playerA, earthElemental, 1); + + attack(1, playerA, terror, playerB); + attack(1, playerA, vanguard, playerB); + attack(1, playerA, earthElemental, playerB); + + setChoice(playerA, "Advanced Species"); // 2 triggers need ordering. + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount(playerA, terror, CounterType.P1P1, 2); + assertLife(playerB, 20 - 4 - 2 - 4); + } + + @Test + public void testNoTriggerNonRedBlockedByRed() { + addCard(Zone.BATTLEFIELD, playerA, terror, 1); + addCard(Zone.BATTLEFIELD, playerA, vanguard, 1); + addCard(Zone.BATTLEFIELD, playerB, earthElemental, 1); + + attack(1, playerA, vanguard, playerB); + block(1, playerB, earthElemental, vanguard); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount(playerA, terror, CounterType.P1P1, 0); + assertLife(playerB, 20); + assertGraveyardCount(playerA, vanguard, 1); + assertDamageReceived(playerB, earthElemental, 2); + } + + @Test + public void testTriggerRedBlockedByRed() { + addCard(Zone.BATTLEFIELD, playerA, terror, 1); + addCard(Zone.BATTLEFIELD, playerA, earthElemental, 1); + addCard(Zone.BATTLEFIELD, playerB, earthElemental, 1); + + attack(1, playerA, earthElemental, playerB); + block(1, playerB, earthElemental, earthElemental); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount(playerA, terror, CounterType.P1P1, 1); + assertLife(playerB, 20); + assertDamageReceived(playerA, earthElemental, 4); + assertDamageReceived(playerB, earthElemental, 4); + } + + @Test + public void testTriggerPyroclasm() { + addCard(Zone.HAND, playerA, pyroclasm, 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.BATTLEFIELD, playerA, terror, 1); + addCard(Zone.BATTLEFIELD, playerA, earthElemental, 1); + addCard(Zone.BATTLEFIELD, playerA, vanguard, 1); + addCard(Zone.BATTLEFIELD, playerB, earthElemental, 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, pyroclasm); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount(playerA, terror, CounterType.P1P1, 1); + assertDamageReceived(playerA, earthElemental, 2); + assertDamageReceived(playerA, terror, 2); + assertGraveyardCount(playerA, vanguard, 1); + assertDamageReceived(playerB, earthElemental, 2); + } + + @Test + public void testTriggerBoltFace() { + addCard(Zone.HAND, playerA, bolt, 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, terror, 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount(playerA, terror, CounterType.P1P1, 1); + assertLife(playerB, 20 - 3); + } + + @Test + public void testTriggerBoltCreature() { + addCard(Zone.HAND, playerA, bolt, 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, terror, 1); + addCard(Zone.BATTLEFIELD, playerA, earthElemental, 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, earthElemental); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount(playerA, terror, CounterType.P1P1, 1); + assertDamageReceived(playerA, earthElemental, 3); + } + + @Test + public void testDoubleTriggerDuelTwoRed() { + addCard(Zone.HAND, playerA, duel, 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + addCard(Zone.BATTLEFIELD, playerA, terror, 1); + addCard(Zone.BATTLEFIELD, playerA, piker, 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, duel, terror + "^" + piker); + + setChoice(playerA, "Advanced Species"); // 2 triggers need ordering. + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount(playerA, terror, CounterType.P1P1, 2); + } + + @Test + public void testTripleTriggerFieryConfluence() { + addCard(Zone.HAND, playerA, confluence, 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + addCard(Zone.BATTLEFIELD, playerA, terror, 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, confluence); + setModeChoice(playerA, "2"); // Fiery Confluence deals 2 damage to each opponent. + setModeChoice(playerA, "2"); // Fiery Confluence deals 2 damage to each opponent. + setModeChoice(playerA, "2"); // Fiery Confluence deals 2 damage to each opponent. + + setChoice(playerA, "Advanced Species", 2); // 3 triggers need ordering. + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertCounterCount(playerA, terror, CounterType.P1P1, 3); + assertLife(playerB, 20 - 2 * 3); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/DealsDamageToAnyTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealsDamageToAnyTriggeredAbility.java index 9f2ff4b1efd..25011ae35e5 100644 --- a/Mage/src/main/java/mage/abilities/common/DealsDamageToAnyTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DealsDamageToAnyTriggeredAbility.java @@ -15,6 +15,8 @@ import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; /** + * Whenever [[filtered permanent]] deals (combat)? damage, [[effect]] + * * @author xenohedron */ public class DealsDamageToAnyTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility {