From f45c9e8ee935c6854b11de579a8d0074a2bfa7cb Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sun, 19 Nov 2023 17:15:11 +0100 Subject: [PATCH] [LCI] Implement Thousand Moons Smithy // Barracks of the Thousand --- .../mage/cards/b/BarracksOfTheThousand.java | 56 +++++++ .../src/mage/cards/t/ThousandMoonsSmithy.java | 68 +++++++++ .../src/mage/sets/TheLostCavernsOfIxalan.java | 2 + .../single/lci/BarracksOfTheThousandTest.java | 141 ++++++++++++++++++ ...CastSpellPaidBySourceTriggeredAbility.java | 77 ++++++++++ .../token/GnomeSoldierStarStarToken.java | 49 ++++++ .../common/ManaPaidObjectSourceWatcher.java | 54 +++++++ 7 files changed, 447 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/b/BarracksOfTheThousand.java create mode 100644 Mage.Sets/src/mage/cards/t/ThousandMoonsSmithy.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/lci/BarracksOfTheThousandTest.java create mode 100644 Mage/src/main/java/mage/abilities/common/CastSpellPaidBySourceTriggeredAbility.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/GnomeSoldierStarStarToken.java create mode 100644 Mage/src/main/java/mage/watchers/common/ManaPaidObjectSourceWatcher.java diff --git a/Mage.Sets/src/mage/cards/b/BarracksOfTheThousand.java b/Mage.Sets/src/mage/cards/b/BarracksOfTheThousand.java new file mode 100644 index 00000000000..5a462272b46 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BarracksOfTheThousand.java @@ -0,0 +1,56 @@ +package mage.cards.b; + +import mage.abilities.common.CastSpellPaidBySourceTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.mana.WhiteManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SuperType; +import mage.filter.FilterSpell; +import mage.filter.predicate.Predicates; +import mage.game.permanent.token.GnomeSoldierStarStarToken; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class BarracksOfTheThousand extends CardImpl { + + private static final FilterSpell filter = new FilterSpell("an artifact or creature spell"); + + static { + filter.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.CREATURE.getPredicate() + )); + } + + public BarracksOfTheThousand(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.LAND}, ""); + + this.supertype.add(SuperType.LEGENDARY); + + // (Transforms from Thousand Moons Smithy.) + this.nightCard = true; + + // {T}: Add {W}. + this.addAbility(new WhiteManaAbility()); + + // Whenever you cast an artifact or creature spell using mana produced by Barracks of the Thousand, create a white Gnome Soldier artifact creature token with "This creature's power and toughness are each equal to the number of artifacts and/or creatures you control." + this.addAbility(new CastSpellPaidBySourceTriggeredAbility( + new CreateTokenEffect(new GnomeSoldierStarStarToken()), + filter, false + )); + } + + private BarracksOfTheThousand(final BarracksOfTheThousand card) { + super(card); + } + + @Override + public BarracksOfTheThousand copy() { + return new BarracksOfTheThousand(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/t/ThousandMoonsSmithy.java b/Mage.Sets/src/mage/cards/t/ThousandMoonsSmithy.java new file mode 100644 index 00000000000..112ba0a0071 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/ThousandMoonsSmithy.java @@ -0,0 +1,68 @@ +package mage.cards.t; + +import mage.abilities.common.BeginningOfPreCombatMainTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.costs.common.TapTargetCost; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SuperType; +import mage.constants.TargetController; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.permanent.TappedPredicate; +import mage.game.permanent.token.GnomeSoldierStarStarToken; +import mage.target.common.TargetControlledPermanent; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class ThousandMoonsSmithy extends CardImpl { + + public static final FilterControlledPermanent filter = + new FilterControlledPermanent("untapped artifacts and/or creatures you control"); + + static { + filter.add(TappedPredicate.UNTAPPED); + filter.add(Predicates.or( + CardType.CREATURE.getPredicate(), + CardType.ARTIFACT.getPredicate() + )); + } + + public ThousandMoonsSmithy(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{W}{W}"); + this.secondSideCardClazz = mage.cards.b.BarracksOfTheThousand.class; + + this.supertype.add(SuperType.LEGENDARY); + + // When Thousand Moons Smithy enters the battlefield, create a white Gnome Soldier artifact creature token with "This creature's power and toughness are each equal to the number of artifacts and/or creatures you control." + this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new GnomeSoldierStarStarToken()))); + + // At the beginning of your precombat main phase, you may tap five untapped artifacts and/or creatures you control. If you do, transform Thousand Moons Smithy. + this.addAbility(new TransformAbility()); + this.addAbility(new BeginningOfPreCombatMainTriggeredAbility( + new DoIfCostPaid( + new TransformSourceEffect(), + new TapTargetCost(new TargetControlledPermanent(5, filter)) + ), + TargetController.YOU, + false + )); + } + + private ThousandMoonsSmithy(final ThousandMoonsSmithy card) { + super(card); + } + + @Override + public ThousandMoonsSmithy copy() { + return new ThousandMoonsSmithy(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java b/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java index e5d0e73e0ca..bbcf61d6fde 100644 --- a/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java +++ b/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java @@ -54,6 +54,7 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet { cards.add(new SetCardInfo("Another Chance", 90, Rarity.COMMON, mage.cards.a.AnotherChance.class)); cards.add(new SetCardInfo("Armored Kincaller", 174, Rarity.COMMON, mage.cards.a.ArmoredKincaller.class)); cards.add(new SetCardInfo("Attentive Sunscribe", 4, Rarity.COMMON, mage.cards.a.AttentiveSunscribe.class)); + cards.add(new SetCardInfo("Barracks of the Thousand", 39, Rarity.RARE, mage.cards.b.BarracksOfTheThousand.class)); cards.add(new SetCardInfo("Bartolome del Presidio", 224, Rarity.UNCOMMON, mage.cards.b.BartolomeDelPresidio.class)); cards.add(new SetCardInfo("Basking Capybara", 175, Rarity.COMMON, mage.cards.b.BaskingCapybara.class)); cards.add(new SetCardInfo("Bat Colony", 5, Rarity.UNCOMMON, mage.cards.b.BatColony.class)); @@ -327,6 +328,7 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet { cards.add(new SetCardInfo("The Skullspore Nexus", 212, Rarity.MYTHIC, mage.cards.t.TheSkullsporeNexus.class)); cards.add(new SetCardInfo("Thousand Moons Crackshot", 37, Rarity.COMMON, mage.cards.t.ThousandMoonsCrackshot.class)); cards.add(new SetCardInfo("Thousand Moons Infantry", 38, Rarity.COMMON, mage.cards.t.ThousandMoonsInfantry.class)); + cards.add(new SetCardInfo("Thousand Moons Smithy", 39, Rarity.RARE, mage.cards.t.ThousandMoonsSmithy.class)); cards.add(new SetCardInfo("Thrashing Brontodon", 216, Rarity.UNCOMMON, mage.cards.t.ThrashingBrontodon.class)); cards.add(new SetCardInfo("Threefold Thunderhulk", 265, Rarity.RARE, mage.cards.t.ThreefoldThunderhulk.class)); cards.add(new SetCardInfo("Throne of the Grim Captain", 266, Rarity.RARE, mage.cards.t.ThroneOfTheGrimCaptain.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/BarracksOfTheThousandTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/BarracksOfTheThousandTest.java new file mode 100644 index 00000000000..e8f4545b1e4 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/BarracksOfTheThousandTest.java @@ -0,0 +1,141 @@ +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 BarracksOfTheThousandTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.t.ThousandMoonsSmithy} {2}{W}{W}
+ * Legendary Artifact
+ * When Thousand Moons Smithy enters the battlefield, create a white Gnome Soldier artifact creature token with “This creature’s power and toughness are each equal to the number of artifacts and/or creatures you control.”
+ * At the beginning of your precombat main phase, you may tap five untapped artifacts and/or creatures you control. If you do, transform Thousand Moons Smithy. + */ + private static final String smithy = "Thousand Moons Smithy"; + /** + * {@link mage.cards.b.BarracksOfTheThousand}
+ * Legendary Artifact Land
+ * {T}: Add {W}.
+ * Whenever you cast an artifact or creature spell using mana produced by Barracks of the Thousand, create a white Gnome Soldier artifact creature token with “This creature’s power and toughness are each equal to the number of artifacts and/or creatures you control.”
+ */ + private static final String barracks = "Barracks of the Thousand"; + + private void initToTransform() { + addCard(Zone.BATTLEFIELD, playerA, smithy); + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears"); + addCard(Zone.BATTLEFIELD, playerA, "Bear Cub"); + addCard(Zone.BATTLEFIELD, playerA, "Forest Bear"); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); + addCard(Zone.BATTLEFIELD, playerA, "Runeclaw Bear"); + + // First mainphase, transform the smithy tapping all the bears + setChoice(playerA, true); // yes to "you may tap" + setChoice(playerA, "Balduvian Bears^Bear Cub^Forest Bear^Grizzly Bears^Runeclaw Bear"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + } + + @Test + public void trigger_simple() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, "Arcbound Worker", 1); + initToTransform(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Arcbound Worker"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Gnome Soldier Token", 1); + assertPermanentCount(playerA, "Arcbound Worker", 1); + } + + @Test + public void trigger_onlyonce_doublemana() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, "Heartbeat of Spring"); + addCard(Zone.HAND, playerA, "Armored Warhorse"); + initToTransform(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Armored Warhorse"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Gnome Soldier Token", 1); + assertPermanentCount(playerA, "Armored Warhorse", 1); + } + + + @Test + public void noTrigger_NotPaidWithBarrack() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, "Memnite", 1); + initToTransform(); + + // Memnite cost 0 so no mana is spend from Barracks. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Memnite"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Gnome Soldier Token", 0); + assertPermanentCount(playerA, "Memnite", 1); + } + + @Test + public void noTrigger_notCheck() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.HAND, playerA, "Divination", 1); + initToTransform(); + + // Sorcery, doesn't trigger Barrack + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Divination"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Gnome Soldier Token", 0); + assertGraveyardCount(playerA, "Divination", 1); + } + + @Test + public void noTrigger_afterRemand() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerB, "Remand"); + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + + addCard(Zone.HAND, playerA, "Arcbound Worker"); + addCard(Zone.HAND, playerA, "Plains", 1); // to cast the second time + initToTransform(); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Arcbound Worker"); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerB, "Remand", "Arcbound Worker", "Arcbound Worker"); + + checkGraveyardCount("Remand in graveyard", 3, PhaseStep.BEGIN_COMBAT, playerB, "Remand", 1); + checkHandCardCount("Worker in hand", 3, PhaseStep.BEGIN_COMBAT, playerA, "Arcbound Worker", 1); + checkPermanentCount("Gnome Soldier Token on battlefield", 3, PhaseStep.BEGIN_COMBAT, playerA, "Gnome Soldier Token", 1); + + playLand(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Plains"); + waitStackResolved(3, PhaseStep.POSTCOMBAT_MAIN); + castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Arcbound Worker"); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Gnome Soldier Token", 1); + assertPermanentCount(playerA, "Arcbound Worker", 1); + assertGraveyardCount(playerB, "Remand", 1); + } + +} diff --git a/Mage/src/main/java/mage/abilities/common/CastSpellPaidBySourceTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/CastSpellPaidBySourceTriggeredAbility.java new file mode 100644 index 00000000000..501f68e9c74 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/CastSpellPaidBySourceTriggeredAbility.java @@ -0,0 +1,77 @@ +package mage.abilities.common; + + +import mage.MageObjectReference; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.filter.FilterSpell; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.stack.Spell; +import mage.target.targetpointer.FixedTarget; +import mage.watchers.common.ManaPaidObjectSourceWatcher; + +/** + * @author Susucr + */ +public class CastSpellPaidBySourceTriggeredAbility extends TriggeredAbilityImpl { + + private final FilterSpell filter; + private final boolean setTargetPointer; + + public CastSpellPaidBySourceTriggeredAbility(Effect effect, FilterSpell filter, boolean setTargetPointer) { + super(Zone.BATTLEFIELD, effect); + setTriggerPhrase("Whenever you cast " + filter.getMessage() + " using mana produced by {this}, "); + addWatcher(new ManaPaidObjectSourceWatcher()); + + this.filter = filter; + this.setTargetPointer = setTargetPointer; + } + + protected CastSpellPaidBySourceTriggeredAbility(final CastSpellPaidBySourceTriggeredAbility ability) { + super(ability); + this.filter = ability.filter; + this.setTargetPointer = ability.setTargetPointer; + } + + @Override + public CastSpellPaidBySourceTriggeredAbility copy() { + return new CastSpellPaidBySourceTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.SPELL_CAST; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!controllerId.equals(event.getPlayerId())) { + return false; + } + + Spell spell = game.getSpell(event.getTargetId()); + if (spell == null || !this.filter.match(spell, controllerId, this, game)) { + return false; + } + + ManaPaidObjectSourceWatcher watcher = game.getState().getWatcher(ManaPaidObjectSourceWatcher.class); + if (watcher == null) { + return false; + } + + if (!watcher.checkManaFromSourceWasUsedToPay( + new MageObjectReference(sourceId, game), + new MageObjectReference(spell.getSourceId(), game) + )) { + return false; + } + + if (setTargetPointer) { + this.getAllEffects().setTargetPointer(new FixedTarget(spell.getId(), game)); + } + + return true; + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/game/permanent/token/GnomeSoldierStarStarToken.java b/Mage/src/main/java/mage/game/permanent/token/GnomeSoldierStarStarToken.java new file mode 100644 index 00000000000..26ce636116f --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/GnomeSoldierStarStarToken.java @@ -0,0 +1,49 @@ +package mage.game.permanent.token; + +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.common.continuous.SetBasePowerToughnessSourceEffect; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; + +/** + * @author Susucr + */ +public final class GnomeSoldierStarStarToken extends TokenImpl { + + private static final FilterControlledPermanent filter = new FilterControlledPermanent(); + + static { + filter.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.CREATURE.getPredicate() + )); + } + + private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(filter); + + public GnomeSoldierStarStarToken() { + super("Gnome Soldier Token", "white Gnome Soldier artifact creature token with " + + "\"this creature's power and toughness are each equal to the number of artifacts and/or creatures you control.\""); + cardType.add(CardType.ARTIFACT); + cardType.add(CardType.CREATURE); + subtype.add(SubType.GNOME); + subtype.add(SubType.SOLDIER); + color.setWhite(true); + + this.addAbility(new SimpleStaticAbility(new SetBasePowerToughnessSourceEffect( + xValue + ).setText("this creature's power and toughness are each equal to the number of artifacts and/or creatures you control"))); + } + + protected GnomeSoldierStarStarToken(final GnomeSoldierStarStarToken token) { + super(token); + } + + public GnomeSoldierStarStarToken copy() { + return new GnomeSoldierStarStarToken(this); + } +} diff --git a/Mage/src/main/java/mage/watchers/common/ManaPaidObjectSourceWatcher.java b/Mage/src/main/java/mage/watchers/common/ManaPaidObjectSourceWatcher.java new file mode 100644 index 00000000000..70e3717f976 --- /dev/null +++ b/Mage/src/main/java/mage/watchers/common/ManaPaidObjectSourceWatcher.java @@ -0,0 +1,54 @@ +package mage.watchers.common; + +import mage.MageObject; +import mage.MageObjectReference; +import mage.constants.WatcherScope; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ManaPaidEvent; +import mage.watchers.Watcher; + +import java.util.*; + +/** + * @author Susucr + */ +public class ManaPaidObjectSourceWatcher extends Watcher { + + // what is paid -> set of all MageObject sources used to pay for the mana. + private final Map> payMap = new HashMap<>(); + + public ManaPaidObjectSourceWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.MANA_PAID) { + return; + } + + ManaPaidEvent manaEvent = (ManaPaidEvent) event; + UUID paid = manaEvent.getSourcePaidId(); + MageObject sourceObject = manaEvent.getSourceObject(); + if (paid == null || sourceObject == null) { + return; + } + + payMap + .computeIfAbsent(new MageObjectReference(paid, game), x -> new HashSet<>()) + .add(new MageObjectReference(sourceObject, game)); + } + + public boolean checkManaFromSourceWasUsedToPay(MageObjectReference sourceOfMana, MageObjectReference paidObject) { + return payMap + .getOrDefault(paidObject, Collections.emptySet()) + .contains(sourceOfMana); + } + + @Override + public void reset() { + payMap.clear(); + super.reset(); + } +}