From 5df9127e01f5ae82a7e8c49b6f1c55bc560db28c Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Tue, 3 Dec 2024 14:18:57 -0600 Subject: [PATCH 1/7] Fixed #13064 --- Mage.Sets/src/mage/cards/e/ElvishBranchbender.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/e/ElvishBranchbender.java b/Mage.Sets/src/mage/cards/e/ElvishBranchbender.java index 93697a497f0..cb7a22c9f91 100644 --- a/Mage.Sets/src/mage/cards/e/ElvishBranchbender.java +++ b/Mage.Sets/src/mage/cards/e/ElvishBranchbender.java @@ -16,8 +16,10 @@ import mage.constants.*; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledPermanent; import mage.game.Game; +import mage.game.permanent.Permanent; import mage.game.permanent.token.TokenImpl; import mage.target.TargetPermanent; +import mage.target.targetpointer.FixedTarget; /** * @@ -78,13 +80,18 @@ class ElvishBranchbenderEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { int xValue = new PermanentsOnBattlefieldCount(filter).calculate(game, source, this); + Permanent targetForest = game.getPermanent(this.getTargetPointer().copy().getFirst(game, source)); + if (targetForest == null) { + return false; + } ContinuousEffect effect = new BecomesCreatureTargetEffect( new ElvishBranchbenderToken(xValue), false, false, Duration.EndOfTurn) .withDurationRuleAtStart(true); - effect.setTargetPointer(this.getTargetPointer().copy()); + // works well with blinked effects + effect.setTargetPointer(new FixedTarget(targetForest, game)); game.addEffect(effect, source); - return false; + return true; } } @@ -101,6 +108,7 @@ class ElvishBranchbenderToken extends TokenImpl { super(token); } + @Override public ElvishBranchbenderToken copy() { return new ElvishBranchbenderToken(this); } -- 2.47.2 From e3a94c8adf050ced88d05f0cb6edcf4894d8d844 Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Wed, 4 Dec 2024 14:23:25 -0600 Subject: [PATCH 2/7] Fixed #13106 --- Mage.Sets/src/mage/cards/p/PolJamaarIllusionist.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/p/PolJamaarIllusionist.java b/Mage.Sets/src/mage/cards/p/PolJamaarIllusionist.java index 5a18bb10191..5ea5e16108a 100644 --- a/Mage.Sets/src/mage/cards/p/PolJamaarIllusionist.java +++ b/Mage.Sets/src/mage/cards/p/PolJamaarIllusionist.java @@ -74,7 +74,8 @@ class PolJamaarIllusionistEffect extends OneShotEffect { } ChoiceCreatureType choice = new ChoiceCreatureType(game, source); player.choose(outcome, choice, game); - SubType subType = SubType.byDescription(choice.getChoice()); + // must use choice.getChoiceKey() so that actual subtype is used + SubType subType = SubType.byDescription(choice.getChoiceKey()); if (subType == null) { return false; } -- 2.47.2 From 29494440897a12a1289207a8c027588bd2544b71 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Thu, 5 Dec 2024 01:03:13 +0400 Subject: [PATCH 3/7] refactor: removed useless code on ChoiceCreatureType usage, removed possibly infinite code (game freezes) (related to #13106); --- Mage.Sets/src/mage/cards/f/ForTheAncestors.java | 4 +++- Mage.Sets/src/mage/cards/h/HarshMercy.java | 10 +++------- Mage.Sets/src/mage/cards/k/KaronaFalseGod.java | 11 ++++------- .../src/mage/cards/l/LongListOfTheEnts.java | 9 +++++---- .../src/mage/cards/m/MistformWakecaster.java | 6 ++---- .../src/mage/cards/p/PatriarchsBidding.java | 5 ++--- .../src/mage/cards/p/PolJamaarIllusionist.java | 6 ++---- Mage.Sets/src/mage/cards/s/Standardize.java | 17 ++++++----------- Mage.Sets/src/mage/cards/t/TribalUnity.java | 4 +--- .../BecomesChosenCreatureTypeTargetEffect.java | 7 +++---- .../java/mage/choices/ChoiceCreatureType.java | 4 +++- .../game/permanent/token/VolosJournalToken.java | 4 +++- 12 files changed, 37 insertions(+), 50 deletions(-) diff --git a/Mage.Sets/src/mage/cards/f/ForTheAncestors.java b/Mage.Sets/src/mage/cards/f/ForTheAncestors.java index ed859b5f147..afffd910b71 100644 --- a/Mage.Sets/src/mage/cards/f/ForTheAncestors.java +++ b/Mage.Sets/src/mage/cards/f/ForTheAncestors.java @@ -69,7 +69,9 @@ class ForTheAncestorsEffect extends OneShotEffect { return false; } ChoiceCreatureType choice = new ChoiceCreatureType(game, source); - player.choose(outcome, choice, game); + if (!player.choose(outcome, choice, game)) { + return false; + } SubType subType = SubType.byDescription(choice.getChoiceKey()); FilterCard filter; if (subType != null) { diff --git a/Mage.Sets/src/mage/cards/h/HarshMercy.java b/Mage.Sets/src/mage/cards/h/HarshMercy.java index 07c738a9d73..4188bcb9088 100644 --- a/Mage.Sets/src/mage/cards/h/HarshMercy.java +++ b/Mage.Sets/src/mage/cards/h/HarshMercy.java @@ -66,18 +66,14 @@ class HarshMercyEffect extends OneShotEffect { MageObject sourceObject = game.getObject(source); if (controller != null && sourceObject != null) { Set chosenTypes = new HashSet<>(); - PlayerIteration: for (UUID playerId : game.getState().getPlayersInRange(controller.getId(), game)) { Player player = game.getPlayer(playerId); Choice typeChoice = new ChoiceCreatureType(game, source); if (player != null && !player.choose(Outcome.DestroyPermanent, typeChoice, game)) { - continue PlayerIteration; - } - String chosenType = typeChoice.getChoiceKey(); - if (chosenType != null) { - game.informPlayers(sourceObject.getIdName() + ": " + player.getLogName() + " has chosen " + chosenType); - chosenTypes.add(chosenType); + continue; } + game.informPlayers(sourceObject.getIdName() + ": " + player.getLogName() + " has chosen " + typeChoice.getChoiceKey()); + chosenTypes.add(typeChoice.getChoiceKey()); } FilterPermanent filter = new FilterCreaturePermanent("creatures"); diff --git a/Mage.Sets/src/mage/cards/k/KaronaFalseGod.java b/Mage.Sets/src/mage/cards/k/KaronaFalseGod.java index 4f7836e5b17..a7a7f61227b 100644 --- a/Mage.Sets/src/mage/cards/k/KaronaFalseGod.java +++ b/Mage.Sets/src/mage/cards/k/KaronaFalseGod.java @@ -128,13 +128,10 @@ class KaronaFalseGodEffect extends OneShotEffect { if (!controller.choose(Outcome.BoostCreature, typeChoice, game)) { return false; } - String typeChosen = typeChoice.getChoiceKey(); - if (!typeChosen.isEmpty()) { - game.informPlayers(controller.getLogName() + " has chosen " + typeChosen); - FilterCreaturePermanent filter = new FilterCreaturePermanent(); - filter.add(SubType.byDescription(typeChosen).getPredicate()); - game.addEffect(new BoostAllEffect(3, 3, Duration.EndOfTurn, filter, false), source); - } + game.informPlayers(controller.getLogName() + " has chosen " + typeChoice.getChoiceKey()); + FilterCreaturePermanent filter = new FilterCreaturePermanent(); + filter.add(SubType.byDescription(typeChoice.getChoiceKey()).getPredicate()); + game.addEffect(new BoostAllEffect(3, 3, Duration.EndOfTurn, filter, false), source); return true; } return false; diff --git a/Mage.Sets/src/mage/cards/l/LongListOfTheEnts.java b/Mage.Sets/src/mage/cards/l/LongListOfTheEnts.java index 12dd936b4d8..aa9b9f02c9e 100644 --- a/Mage.Sets/src/mage/cards/l/LongListOfTheEnts.java +++ b/Mage.Sets/src/mage/cards/l/LongListOfTheEnts.java @@ -109,7 +109,7 @@ class LongListOfTheEntsEffect extends OneShotEffect { if (player == null) { return false; } - ChoiceCreatureType choice = new ChoiceCreatureType(game, source); + Object existingEntList = game.getState().getValue(LongListOfTheEnts.getKey(game, source, 0)); int offset; Set newEntList; @@ -124,12 +124,13 @@ class LongListOfTheEntsEffect extends OneShotEffect { .stream() .map(SubType::toString) .collect(Collectors.toSet()); + + ChoiceCreatureType choice = new ChoiceCreatureType(game, source); choice.getKeyChoices().keySet().removeIf(chosenTypes::contains); - player.choose(Outcome.BoostCreature, choice, game); - SubType subType = SubType.byDescription(choice.getChoiceKey()); - if (subType == null) { + if (!player.choose(Outcome.BoostCreature, choice, game)) { return false; } + SubType subType = SubType.byDescription(choice.getChoiceKey()); game.informPlayers(player.getLogName() + " notes the creature type " + subType); newEntList.add(subType); game.getState().setValue(LongListOfTheEnts.getKey(game, source, offset), newEntList); diff --git a/Mage.Sets/src/mage/cards/m/MistformWakecaster.java b/Mage.Sets/src/mage/cards/m/MistformWakecaster.java index 3ccc7f900bd..a8fc675fcf1 100644 --- a/Mage.Sets/src/mage/cards/m/MistformWakecaster.java +++ b/Mage.Sets/src/mage/cards/m/MistformWakecaster.java @@ -78,10 +78,8 @@ class BecomesChosenCreatureTypeControlledEffect extends OneShotEffect { String chosenType = ""; if (player != null && card != null) { Choice typeChoice = new ChoiceCreatureType(game, source); - while (!player.choose(Outcome.BoostCreature, typeChoice, game)) { - if (!player.canRespond()) { - return false; - } + if (!player.choose(Outcome.BoostCreature, typeChoice, game)) { + return false; } game.informPlayers(card.getName() + ": " + player.getLogName() + " has chosen " + typeChoice.getChoiceKey()); chosenType = typeChoice.getChoiceKey(); diff --git a/Mage.Sets/src/mage/cards/p/PatriarchsBidding.java b/Mage.Sets/src/mage/cards/p/PatriarchsBidding.java index 6e27d7fa8f0..d29629da719 100644 --- a/Mage.Sets/src/mage/cards/p/PatriarchsBidding.java +++ b/Mage.Sets/src/mage/cards/p/PatriarchsBidding.java @@ -69,9 +69,8 @@ class PatriarchsBiddingEffect extends OneShotEffect { if (!player.choose(Outcome.PutCreatureInPlay, typeChoice, game)) { continue; } - String chosenType = typeChoice.getChoiceKey(); - game.informPlayers(sourceObject.getLogName() + ": " + player.getLogName() + " has chosen " + chosenType); - chosenTypes.add(chosenType); + game.informPlayers(sourceObject.getLogName() + ": " + player.getLogName() + " has chosen " + typeChoice.getChoiceKey()); + chosenTypes.add(typeChoice.getChoiceKey()); } List predicates = new ArrayList<>(); diff --git a/Mage.Sets/src/mage/cards/p/PolJamaarIllusionist.java b/Mage.Sets/src/mage/cards/p/PolJamaarIllusionist.java index 5ea5e16108a..7ab00e4ada3 100644 --- a/Mage.Sets/src/mage/cards/p/PolJamaarIllusionist.java +++ b/Mage.Sets/src/mage/cards/p/PolJamaarIllusionist.java @@ -73,12 +73,10 @@ class PolJamaarIllusionistEffect extends OneShotEffect { return false; } ChoiceCreatureType choice = new ChoiceCreatureType(game, source); - player.choose(outcome, choice, game); - // must use choice.getChoiceKey() so that actual subtype is used - SubType subType = SubType.byDescription(choice.getChoiceKey()); - if (subType == null) { + if (!player.choose(outcome, choice, game)) { return false; } + SubType subType = SubType.byDescription(choice.getChoiceKey()); game.informPlayers(player.getLogName() + " chooses " + subType); int amount = game .getBattlefield() diff --git a/Mage.Sets/src/mage/cards/s/Standardize.java b/Mage.Sets/src/mage/cards/s/Standardize.java index fe413cb179f..53b7a86a21e 100644 --- a/Mage.Sets/src/mage/cards/s/Standardize.java +++ b/Mage.Sets/src/mage/cards/s/Standardize.java @@ -57,7 +57,6 @@ class StandardizeEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); MageObject sourceObject = game.getObject(source); - String chosenType = ""; if (player != null && sourceObject != null) { Choice typeChoice = new ChoiceCreatureType(game, source); typeChoice.setMessage("Choose a creature type other than Wall"); @@ -66,16 +65,12 @@ class StandardizeEffect extends OneShotEffect { return false; } game.informPlayers(sourceObject.getLogName() + ": " + player.getLogName() + " has chosen " + typeChoice.getChoiceKey()); - chosenType = typeChoice.getChoiceKey(); - if (chosenType != null && !chosenType.isEmpty()) { - // ADD TYPE TO TARGET - game.addEffect(new BecomesSubtypeAllEffect( - Duration.EndOfTurn, Arrays.asList(SubType.byDescription(chosenType)), - StaticFilters.FILTER_PERMANENT_CREATURE, true - ), source); - return true; - } - + // ADD TYPE TO TARGET + game.addEffect(new BecomesSubtypeAllEffect( + Duration.EndOfTurn, Arrays.asList(SubType.byDescription(typeChoice.getChoiceKey())), + StaticFilters.FILTER_PERMANENT_CREATURE, true + ), source); + return true; } return false; } diff --git a/Mage.Sets/src/mage/cards/t/TribalUnity.java b/Mage.Sets/src/mage/cards/t/TribalUnity.java index 9e0829c0672..93babcb756a 100644 --- a/Mage.Sets/src/mage/cards/t/TribalUnity.java +++ b/Mage.Sets/src/mage/cards/t/TribalUnity.java @@ -67,9 +67,7 @@ class TribalUnityEffect extends OneShotEffect { Choice typeChoice = new ChoiceCreatureType(game, source); if (player != null && player.choose(outcome, typeChoice, game)) { int boost = amount.calculate(game, source, this); - if (typeChoice.getChoiceKey() != null) { - game.informPlayers(sourceObject.getLogName() + " chosen type: " + typeChoice.getChoiceKey()); - } + game.informPlayers(sourceObject.getLogName() + " chosen type: " + typeChoice.getChoiceKey()); FilterCreaturePermanent filterCreaturePermanent = new FilterCreaturePermanent(); filterCreaturePermanent.add(SubType.byDescription(typeChoice.getChoiceKey()).getPredicate()); game.addEffect(new BoostAllEffect( diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesChosenCreatureTypeTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesChosenCreatureTypeTargetEffect.java index 259ecd0d490..474a0b93733 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesChosenCreatureTypeTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesChosenCreatureTypeTargetEffect.java @@ -59,10 +59,9 @@ public class BecomesChosenCreatureTypeTargetEffect extends OneShotEffect { if (nonWall) { typeChoice.getKeyChoices().remove(SubType.WALL.getDescription()); } - while (!player.choose(Outcome.BoostCreature, typeChoice, game)) { - if (!player.canRespond()) { - return false; - } + + if (!player.choose(Outcome.BoostCreature, typeChoice, game)) { + return false; } game.informPlayers(card.getName() + ": " + player.getLogName() + " has chosen " + typeChoice.getChoiceKey()); chosenType = typeChoice.getChoiceKey(); diff --git a/Mage/src/main/java/mage/choices/ChoiceCreatureType.java b/Mage/src/main/java/mage/choices/ChoiceCreatureType.java index 2de289eab6f..733bb37ca78 100644 --- a/Mage/src/main/java/mage/choices/ChoiceCreatureType.java +++ b/Mage/src/main/java/mage/choices/ChoiceCreatureType.java @@ -10,7 +10,9 @@ import java.util.*; import java.util.stream.Collectors; /** - * Game's choose dialog to ask about creature type. Return getChoice + * Game's choose dialog to ask about creature type. + *

+ * Warning, must use getChoiceKey as result */ public class ChoiceCreatureType extends ChoiceImpl { diff --git a/Mage/src/main/java/mage/game/permanent/token/VolosJournalToken.java b/Mage/src/main/java/mage/game/permanent/token/VolosJournalToken.java index 44ea1af0e2b..e263f028106 100644 --- a/Mage/src/main/java/mage/game/permanent/token/VolosJournalToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/VolosJournalToken.java @@ -135,7 +135,9 @@ class VolosJournalTokenEffect extends OneShotEffect { return true; } - player.choose(outcome, choice, game); + if (!player.choose(outcome, choice, game)) { + return false; + } notedTypes.add(choice.getChoiceKey()); return true; } -- 2.47.2 From b571080260a15b3aa022baa513fe735a4553ed1e Mon Sep 17 00:00:00 2001 From: Jeff Wadsworth Date: Wed, 4 Dec 2024 16:06:46 -0600 Subject: [PATCH 4/7] Fixed #13056 --- Mage.Sets/src/mage/cards/c/ComeBackWrong.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/c/ComeBackWrong.java b/Mage.Sets/src/mage/cards/c/ComeBackWrong.java index cd7d5569031..8ec12a76218 100644 --- a/Mage.Sets/src/mage/cards/c/ComeBackWrong.java +++ b/Mage.Sets/src/mage/cards/c/ComeBackWrong.java @@ -17,6 +17,7 @@ import mage.target.common.TargetCreaturePermanent; import mage.target.targetpointer.FixedTarget; import java.util.UUID; +import mage.game.permanent.PermanentToken; /** * @author TheElk801 @@ -44,7 +45,7 @@ public final class ComeBackWrong extends CardImpl { class ComeBackWrongEffect extends OneShotEffect { ComeBackWrongEffect() { - super(Outcome.Benefit); + super(Outcome.Neutral); staticText = "destroy target creature. If a creature card is put into a graveyard this way, " + "return it to the battlefield under your control. Sacrifice it at the beginning of your next end step"; } @@ -65,8 +66,14 @@ class ComeBackWrongEffect extends OneShotEffect { return false; } permanent.destroy(source, game); + // tokens are not creature cards + if (permanent instanceof PermanentToken) { + return false; + } Card card = permanent.getMainCard(); - if (card == null || !card.isCreature(game) || !Zone.GRAVEYARD.match(game.getState().getZone(card.getId()))) { + if (card == null + || !card.isCreature(game) + || !Zone.GRAVEYARD.match(game.getState().getZone(card.getId()))) { return true; } Player player = game.getPlayer(source.getControllerId()); -- 2.47.2 From 8af7a492c801e762023dbeb42f3fbd4f9c00e2f6 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 7 Dec 2024 14:39:52 +0400 Subject: [PATCH 5/7] refactor: fixed dies events support in single cards (part 7, related to #13089, continue from #13088); --- .../cards/n/NadierAgentOfTheDuskenel.java | 3 + .../src/mage/cards/t/ThreeTreeScribe.java | 1 + .../src/mage/cards/v/ValorOfTheWorthy.java | 1 + .../cmr/NadierAgentOfTheDuskenelTest.java | 83 +++++++++++++++++++ .../single/khm/ValorOfTheWorthyTest.java | 70 ++++++++++++++++ .../java/mage/verify/VerifyCardDataTest.java | 4 +- .../LeavesBattlefieldAllTriggeredAbility.java | 1 + 7 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/cmr/NadierAgentOfTheDuskenelTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/khm/ValorOfTheWorthyTest.java diff --git a/Mage.Sets/src/mage/cards/n/NadierAgentOfTheDuskenel.java b/Mage.Sets/src/mage/cards/n/NadierAgentOfTheDuskenel.java index e459b12dffa..c327d112c8c 100644 --- a/Mage.Sets/src/mage/cards/n/NadierAgentOfTheDuskenel.java +++ b/Mage.Sets/src/mage/cards/n/NadierAgentOfTheDuskenel.java @@ -83,6 +83,9 @@ class NadierAgentOfTheDuskenelEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { + // For Nadier's second ability, use its power from when it was last on the battlefield to determine + // how many tokens to create. + // (2020-11-10) Object obj = getValue("permanentLeftBattlefield"); if (!(obj instanceof Permanent)) { return false; diff --git a/Mage.Sets/src/mage/cards/t/ThreeTreeScribe.java b/Mage.Sets/src/mage/cards/t/ThreeTreeScribe.java index 5cc58e66ead..b443b915419 100644 --- a/Mage.Sets/src/mage/cards/t/ThreeTreeScribe.java +++ b/Mage.Sets/src/mage/cards/t/ThreeTreeScribe.java @@ -50,6 +50,7 @@ class ThreeTreeScribeTriggeredAbility extends TriggeredAbilityImpl { super(Zone.BATTLEFIELD, new AddCountersTargetEffect(CounterType.P1P1.createInstance())); this.setTriggerPhrase("Whenever {this} or another creature you control leaves the battlefield without dying, "); this.addTarget(new TargetControlledCreaturePermanent()); + setLeavesTheBattlefieldTrigger(true); } private ThreeTreeScribeTriggeredAbility(final ThreeTreeScribeTriggeredAbility ability) { diff --git a/Mage.Sets/src/mage/cards/v/ValorOfTheWorthy.java b/Mage.Sets/src/mage/cards/v/ValorOfTheWorthy.java index d34eec16a03..fa7d0c49398 100644 --- a/Mage.Sets/src/mage/cards/v/ValorOfTheWorthy.java +++ b/Mage.Sets/src/mage/cards/v/ValorOfTheWorthy.java @@ -61,6 +61,7 @@ class LeavesTheBattlefieldAttachedTriggeredAbility extends ZoneChangeTriggeredAb public LeavesTheBattlefieldAttachedTriggeredAbility() { super(Zone.BATTLEFIELD, new CreateTokenEffect(new SpiritWhiteToken()), "When enchanted creature leaves the battlefield, ", Boolean.FALSE); + setLeavesTheBattlefieldTrigger(true); } private LeavesTheBattlefieldAttachedTriggeredAbility(final LeavesTheBattlefieldAttachedTriggeredAbility ability) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/cmr/NadierAgentOfTheDuskenelTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/cmr/NadierAgentOfTheDuskenelTest.java new file mode 100644 index 00000000000..430ae373409 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/cmr/NadierAgentOfTheDuskenelTest.java @@ -0,0 +1,83 @@ +package org.mage.test.cards.single.cmr; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommanderDuelBase; + +/** + * @author JayDi85 + */ + +public class NadierAgentOfTheDuskenelTest extends CardTestCommanderDuelBase { + + @Test + public void test_DieAnother() { + addCustomEffect_TargetDestroy(playerA); + + // Whenever a token you control leaves the battlefield, put a +1/+1 counter on Nadier, Agent of the Duskenel. + // When Nadier leaves the battlefield, create a number of 1/1 green Elf Warrior creature tokens equal to its power. + addCard(Zone.BATTLEFIELD, playerA, "Nadier, Agent of the Duskenel", 1); + // + // {5}, {T}: Create a 1/1 colorless Insect artifact creature token with flying named Wasp. + addCard(Zone.BATTLEFIELD, playerA, "The Hive", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + + // prepare token + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}, {T}: Create"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wasp", 1); + + // on leaves non-token -- nothing to happen + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target destroy", "The Hive"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); + checkGraveyardCount("on non-token", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Hive", 1); + checkStackSize("on non-token - no triggers", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 0); + + // on leaves token - must trigger + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target destroy", "Wasp"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("on token", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wasp", 0); + checkPermanentCounters("on token - must trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nadier, Agent of the Duskenel", CounterType.P1P1, 1); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + } + + @Test + public void test_DieItself() { + addCustomEffect_TargetDestroy(playerA, 2); + + // Whenever a token you control leaves the battlefield, put a +1/+1 counter on Nadier, Agent of the Duskenel. + // When Nadier leaves the battlefield, create a number of 1/1 green Elf Warrior creature tokens equal to its power. + addCard(Zone.BATTLEFIELD, playerA, "Nadier, Agent of the Duskenel", 1); + // + // {5}, {T}: Create a 1/1 colorless Insect artifact creature token with flying named Wasp. + addCard(Zone.BATTLEFIELD, playerA, "The Hive", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + + // prepare token + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}, {T}: Create"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wasp", 1); + + // on leaves token and itself -- must x2 triggers: + // * one with counter to fizzle + // * one with new token + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target destroy", "Wasp^Nadier, Agent of the Duskenel"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); + checkStackSize("must triggers x2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); + setChoice(playerA, "Whenever a token you control leaves"); // x2 triggers from Nadier + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "The Hive", 1); + assertPermanentCount(playerA, "Wasp", 0); + assertGraveyardCount(playerA, "Nadier, Agent of the Duskenel", 1); + assertPermanentCount(playerA, "Elf Warrior Token", 3); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/ValorOfTheWorthyTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/ValorOfTheWorthyTest.java new file mode 100644 index 00000000000..765efe44c1f --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/ValorOfTheWorthyTest.java @@ -0,0 +1,70 @@ +package org.mage.test.cards.single.khm; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class ValorOfTheWorthyTest extends CardTestPlayerBase { + + @Test + public void test_DieTarget() { + addCustomEffect_TargetDestroy(playerA); + + // Enchant creature + // Enchanted creature gets +1/+1. + // When enchanted creature leaves the battlefield, create a 1/1 white Spirit creature token with flying. + addCard(Zone.HAND, playerA, "Valor of the Worthy"); // {W} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); + + // attach + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Valor of the Worthy", "Grizzly Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + checkPT("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears", 2 + 1, 2 + 1); + + // destroy target - must trigger + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target destroy"); + addTarget(playerA, "Grizzly Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); // resolve destroy + checkStackObject("must trigger on destroy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "When enchanted creature leaves the battlefield", 1); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTokenCount(playerA, "Spirit Token", 1); + } + + @Test + public void test_DieItself() { + addCustomEffect_TargetDestroy(playerA, 2); + + // Enchant creature + // Enchanted creature gets +1/+1. + // When enchanted creature leaves the battlefield, create a 1/1 white Spirit creature token with flying. + addCard(Zone.HAND, playerA, "Valor of the Worthy"); // {W} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); + + // attach + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Valor of the Worthy", "Grizzly Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + checkPT("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears", 2 + 1, 2 + 1); + + // destroy all - must trigger anyway + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target destroy"); + addTarget(playerA, "Valor of the Worthy^Grizzly Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); // resolve destroy + checkStackObject("must trigger on destroy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "When enchanted creature leaves the battlefield", 1); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTokenCount(playerA, "Spirit Token", 1); + } +} diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index e018ec86f55..2f42b8867a0 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -2021,10 +2021,12 @@ public class VerifyCardDataTest { boolean isPutToGraveAbility = rules.contains("put into") && rules.contains("graveyard") && rules.contains("from the battlefield"); + boolean isLeavesBattlefield = rules.contains("leaves the battlefield"); + isLeavesBattlefield = false; // TODO: remove and fix all bad cards if (triggeredAbility.isLeavesTheBattlefieldTrigger()) { // TODO: add check for wrongly enabled settings too? } else { - if (isDiesAbility || isPutToGraveAbility) { + if (isDiesAbility || isPutToGraveAbility || isLeavesBattlefield) { fail(card, "abilities", "dies related trigger must use setLeavesTheBattlefieldTrigger(true) and possibly override isInUseableZone - " + triggeredAbility.getClass().getSimpleName()); } diff --git a/Mage/src/main/java/mage/abilities/common/LeavesBattlefieldAllTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/LeavesBattlefieldAllTriggeredAbility.java index 139059b683b..b67304bd323 100644 --- a/Mage/src/main/java/mage/abilities/common/LeavesBattlefieldAllTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/LeavesBattlefieldAllTriggeredAbility.java @@ -39,6 +39,7 @@ public class LeavesBattlefieldAllTriggeredAbility extends TriggeredAbilityImpl { this.filter = filter; this.setTargetPointer = setTargetPointer; setTriggerPhrase("Whenever " + filter.getMessage() + " leaves the battlefield, "); + setLeavesTheBattlefieldTrigger(true); } protected LeavesBattlefieldAllTriggeredAbility(final LeavesBattlefieldAllTriggeredAbility ability) { -- 2.47.2 From de34a98208d86d7803b8792c1a36f811b01be535 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 7 Dec 2024 14:54:02 +0400 Subject: [PATCH 6/7] client: added java version and charset info to error report (related to #13121) --- .../src/main/java/mage/client/dialog/ErrorDialog.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Mage.Client/src/main/java/mage/client/dialog/ErrorDialog.java b/Mage.Client/src/main/java/mage/client/dialog/ErrorDialog.java index f589ea9a890..cae3b9a8eff 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/ErrorDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/ErrorDialog.java @@ -5,6 +5,8 @@ import mage.client.util.AppUtil; import mage.client.util.GUISizeHelper; import mage.util.CardUtil; +import java.nio.charset.Charset; + /** * GUI: error dialog with copyable error message * // TODO: add game logs and data for game errors (client side info from GameView) @@ -28,7 +30,10 @@ public class ErrorDialog extends MageDialog { // add additional info String fullError = "Error type: " + fullTitle + "\n" + + "\n" + "Client version: " + MageFrame.getInstance().getVersion().toString() + "\n" + + "Java version: " + System.getProperty("java.version") + "\n" + + "Default charset: " + Charset.defaultCharset() + "\n" + "\n" + errorText; this.textError.setText(fullError); @@ -68,7 +73,8 @@ public class ErrorDialog extends MageDialog { AppUtil.openUrlInSystemBrowser(url); } - /** This method is called from within the constructor to + /** + * This method is called from within the constructor to * initialize the form. * WARNING: Do NOT modify this code. The content of this method is * always regenerated by the Form Editor. -- 2.47.2 From 9816ec7c26aeac588a35e56a781fc897b9809cf8 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 7 Dec 2024 22:38:43 +0400 Subject: [PATCH 7/7] tests: added wrong commands order check --- .../cards/single/mh3/PrimalPrayersTest.java | 2 +- .../cards/triggers/dies/BloodCultistTest.java | 3 ++- .../org/mage/test/player/PlayerAction.java | 4 +++- .../base/impl/CardTestPlayerAPIImpl.java | 22 +++++++++++++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/PrimalPrayersTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/PrimalPrayersTest.java index 3142f9a7655..6340911e153 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/PrimalPrayersTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/PrimalPrayersTest.java @@ -86,7 +86,7 @@ public class PrimalPrayersTest extends CardTestPlayerBase { setChoice(playerA, "Cast with alternative cost: Pay {E}"); // alternative cost chosen checkPlayableAbility("no more energy to cast third Bears", 1, PhaseStep.BEGIN_COMBAT, playerA, "Cast Grizzly Bears", false); - runCode("3: energy counter is 0", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> checkEnergyCount(info, player, 0)); + runCode("3: energy counter is 0", 1, PhaseStep.BEGIN_COMBAT, playerA, (info, player, game) -> checkEnergyCount(info, player, 0)); setStopAt(1, PhaseStep.END_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/BloodCultistTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/BloodCultistTest.java index 209d9cb5d99..f521ea851f3 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/BloodCultistTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/BloodCultistTest.java @@ -102,10 +102,11 @@ public class BloodCultistTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Blood Cultist"); activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: {this} deals", "Devilthorn Fox"); - activateAbility(5, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: {this} deals", "Shambling Ghoul"); attack(5, playerA, "Silvercoat Lion"); block(5, playerB, "Shambling Ghoul", "Silvercoat Lion"); + activateAbility(5, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: {this} deals", "Shambling Ghoul"); + setStrictChooseMode(true); setStopAt(5, PhaseStep.END_TURN); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/player/PlayerAction.java b/Mage.Tests/src/test/java/org/mage/test/player/PlayerAction.java index 3e4c7405fdc..5b0fbf2cb72 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/PlayerAction.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/PlayerAction.java @@ -57,6 +57,8 @@ public class PlayerAction { @Override public String toString() { - return "T" + this.turnNum + "." + this.step.getStepShortText() + ": " + this.action; + return "T" + this.turnNum + "." + this.step.getStepShortText() + + ": " + this.action + + (this.actionName.isEmpty() ? "" : " (" + this.actionName + ")"); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 03b7fd69013..f13f18f145f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -262,6 +262,28 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement + " (found actions after stop on " + maxTurn + " / " + maxPhase + ")", (maxTurn > this.stopOnTurn) || (maxTurn == this.stopOnTurn && maxPhase > this.stopAtStep.getIndex())); + // check commands order + for (Player player : currentGame.getPlayers().values()) { + if (true) break; // TODO: delete/comment and fix all failed tests + if (player instanceof TestPlayer) { + TestPlayer testPlayer = (TestPlayer) player; + int lastActionIndex = 0; + PlayerAction lastAction = null; + for (PlayerAction currentAction : testPlayer.getActions()) { + int currentActionIndex = 1000 * currentAction.getTurnNum() + currentAction.getStep().getIndex(); + if (currentActionIndex < lastActionIndex) { + // how-to fix: find typo in step/turn number + Assert.fail("Found wrong commands order for " + testPlayer.getName() + ":" + "\n" + + lastAction + "\n" + + currentAction); + } else { + lastActionIndex = currentActionIndex; + lastAction = currentAction; + } + } + } + } + if (!currentGame.isPaused()) { // workaround to fill range info (cause real range fills after game start, but some cheated cards needs range on ETB) for (Player player : currentGame.getPlayers().values()) { -- 2.47.2