From 33380f09c2d83e25aa3e535967e93ceca12872c0 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 21 Aug 2021 01:44:00 +0400 Subject: [PATCH 01/17] Improved canActivate support: * added support of non controller activates in ActivatedManaAbility (mayActivate); * removed custom code from ActivatedManaAbility; * removed custom code from Mana Cache; * added additional comments; --- .../src/mage/cards/g/GlimpseTheCosmos.java | 2 ++ Mage.Sets/src/mage/cards/m/ManaCache.java | 22 ++++++++----------- .../java/mage/abilities/ActivatedAbility.java | 5 +++++ .../mage/abilities/ActivatedAbilityImpl.java | 2 +- .../java/mage/abilities/PlayLandAbility.java | 8 +++++++ .../java/mage/abilities/SpellAbility.java | 5 ++++- .../mana/ActivatedManaAbilityImpl.java | 15 +------------ .../main/java/mage/players/PlayerImpl.java | 3 +++ 8 files changed, 33 insertions(+), 29 deletions(-) diff --git a/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java b/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java index 8a46054c527..e40b1525b49 100644 --- a/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java +++ b/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java @@ -53,6 +53,7 @@ public class GlimpseTheCosmos extends CardImpl { } +// TODO: card can't use custom SpellAbility, must be reworked to PLAY_FROM_NOT_OWN_HAND_ZONE (example: Worldheart Phoenix) class GlimpseTheCosmosAbility extends SpellAbility { private String abilityName; @@ -74,6 +75,7 @@ class GlimpseTheCosmosAbility extends SpellAbility { @Override public ActivationStatus canActivate(UUID playerId, Game game) { + // TODO: miss super.canActivate? Card card = game.getCard(getSourceId()); if (card != null) { // Card must be in the graveyard zone diff --git a/Mage.Sets/src/mage/cards/m/ManaCache.java b/Mage.Sets/src/mage/cards/m/ManaCache.java index 2ee9c317a73..716ae7d3050 100644 --- a/Mage.Sets/src/mage/cards/m/ManaCache.java +++ b/Mage.Sets/src/mage/cards/m/ManaCache.java @@ -11,10 +11,7 @@ import mage.abilities.effects.mana.BasicManaEffect; import mage.abilities.mana.ActivatedManaAbilityImpl; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Outcome; -import mage.constants.PhaseStep; -import mage.constants.Zone; +import mage.constants.*; import mage.counters.CounterType; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledLandPermanent; @@ -94,6 +91,7 @@ class ManaCacheManaAbility extends ActivatedManaAbilityImpl { super(Zone.BATTLEFIELD, new BasicManaEffect(Mana.ColorlessMana(1), new CountersSourceCount(CounterType.CHARGE)), new RemoveCountersSourceCost(CounterType.CHARGE.createInstance(1))); this.netMana.add(new Mana(0, 0, 0, 0, 0, 0, 0, 1)); + this.setMayActivate(TargetController.ANY); } public ManaCacheManaAbility(final ManaCacheManaAbility ability) { @@ -102,17 +100,15 @@ class ManaCacheManaAbility extends ActivatedManaAbilityImpl { @Override public ActivationStatus canActivate(UUID playerId, Game game) { - if (!super.hasMoreActivationsThisTurn(game) || !(condition == null || condition.apply(game, this))) { + // any player, but only during their turn before the end step + Player player = game.getPlayer(playerId); + if (player == null + || !playerId.equals(game.getActivePlayerId()) + || !game.getStep().getType().isBefore(PhaseStep.END_TURN)) { return ActivationStatus.getFalse(); } - Player player = game.getPlayer(playerId); - if (player != null && playerId.equals(game.getActivePlayerId()) && game.getStep().getType().isBefore(PhaseStep.END_TURN)) { - if (costs.canPay(this, this, playerId, game)) { - this.setControllerId(playerId); - return ActivationStatus.getTrue(this, game); - } - } - return ActivationStatus.getFalse(); + + return super.canActivate(playerId, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/ActivatedAbility.java b/Mage/src/main/java/mage/abilities/ActivatedAbility.java index 29e8d90bace..61fe629ab16 100644 --- a/Mage/src/main/java/mage/abilities/ActivatedAbility.java +++ b/Mage/src/main/java/mage/abilities/ActivatedAbility.java @@ -53,6 +53,11 @@ public interface ActivatedAbility extends Ability { */ ActivationStatus canActivate(UUID playerId, Game game); // has to return a reference to the permitting ability/source + /** + * Who can activate an ability. By default, only you (the controller/owner). + * + * @param mayActivate + */ void setMayActivate(TargetController mayActivate); /** diff --git a/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java b/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java index 08d9f33d140..2149d7fe87d 100644 --- a/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java @@ -173,7 +173,7 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa } /** - * Basic activation check. It contains costs and targets legality too. + * Activated ability check, not spells. It contains costs and targets legality too. *

* WARNING, don't forget to call super.canActivate on override in card's code in most cases. * diff --git a/Mage/src/main/java/mage/abilities/PlayLandAbility.java b/Mage/src/main/java/mage/abilities/PlayLandAbility.java index 0b8c3df6167..750ba6dc543 100644 --- a/Mage/src/main/java/mage/abilities/PlayLandAbility.java +++ b/Mage/src/main/java/mage/abilities/PlayLandAbility.java @@ -25,6 +25,14 @@ public class PlayLandAbility extends ActivatedAbilityImpl { @Override public ActivationStatus canActivate(UUID playerId, Game game) { + // 20210723 - 116.2a + // Playing a land is a special action. To play a land, a player puts that land onto the battlefield + // from the zone it was in (usually that player’s hand). By default, a player can take this action + // only once during each of their turns. A player can take this action any time they have priority + // and the stack is empty during a main phase of their turn. See rule 305, “Lands.” + + // no super.canActivate() call + ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); if (!controlsAbility(playerId, game) && null == approvingObject) { return ActivationStatus.getFalse(); diff --git a/Mage/src/main/java/mage/abilities/SpellAbility.java b/Mage/src/main/java/mage/abilities/SpellAbility.java index 368af5b155f..4ab4c6cecf9 100644 --- a/Mage/src/main/java/mage/abilities/SpellAbility.java +++ b/Mage/src/main/java/mage/abilities/SpellAbility.java @@ -87,6 +87,9 @@ public class SpellAbility extends ActivatedAbilityImpl { @Override public ActivationStatus canActivate(UUID playerId, Game game) { + // spells can be cast from non hand zones, so must use custom check + // no super.canActivate() call + if (this.spellCanBeActivatedRegularlyNow(playerId, game)) { if (spellAbilityType == SpellAbilityType.SPLIT || spellAbilityType == SpellAbilityType.SPLIT_AFTERMATH) { @@ -121,7 +124,7 @@ public class SpellAbility extends ActivatedAbilityImpl { } } - // can pay all costs + // can pay all costs and choose targets if (costs.canPay(this, this, playerId, game)) { if (getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) { SplitCard splitCard = (SplitCard) game.getCard(getSourceId()); diff --git a/Mage/src/main/java/mage/abilities/mana/ActivatedManaAbilityImpl.java b/Mage/src/main/java/mage/abilities/mana/ActivatedManaAbilityImpl.java index 53ebed55211..f6fd9becc71 100644 --- a/Mage/src/main/java/mage/abilities/mana/ActivatedManaAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/mana/ActivatedManaAbilityImpl.java @@ -42,18 +42,6 @@ public abstract class ActivatedManaAbilityImpl extends ActivatedAbilityImpl impl @Override public ActivationStatus canActivate(UUID playerId, Game game) { - if (!super.hasMoreActivationsThisTurn(game) || !(condition == null || condition.apply(game, this))) { - return ActivationStatus.getFalse(); - } - if (!controlsAbility(playerId, game)) { - return ActivationStatus.getFalse(); - } - if (timing == TimingRule.SORCERY - && !game.canPlaySorcery(playerId) - && null == game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.ACTIVATE_AS_INSTANT, this, controllerId, game)) { - return ActivationStatus.getFalse(); - } - // check if player is in the process of playing spell costs and they are no longer allowed to use // activated mana abilities (e.g. because they started to use improvise or convoke) if (!game.getStack().isEmpty()) { @@ -69,8 +57,7 @@ public abstract class ActivatedManaAbilityImpl extends ActivatedAbilityImpl impl } } - //20091005 - 605.3a - return new ActivationStatus(costs.canPay(this, this, controllerId, game), null); + return super.canActivate(playerId, game); } /** diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index fccfebad493..4dc9bbc5630 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -3706,6 +3706,9 @@ public abstract class PlayerImpl implements Player, Serializable { } // check the hand zone (Sen Triplets) + // TODO: remove direct hand check (reveal fix in Sen Triplets)? + // human games: cards from opponent's hand must be revealed before play + // AI games: computer can see and play cards from opponent's hand without reveal if (fromAll || fromZone == Zone.HAND) { for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { Player player = game.getPlayer(playerInRangeId); From 07d0e590a97c418e02532c90af07d879e60c7dc1 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 21 Aug 2021 11:19:34 +0400 Subject: [PATCH 02/17] Other: fixed rare NPE error on wrong connection, fixed formal param in Aminatou, the Fateshifter; --- Mage.Common/src/main/java/mage/remote/SessionImpl.java | 2 +- Mage.Sets/src/mage/cards/a/AminatouTheFateshifter.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Mage.Common/src/main/java/mage/remote/SessionImpl.java b/Mage.Common/src/main/java/mage/remote/SessionImpl.java index a2b3b45c7fe..4b730261dd6 100644 --- a/Mage.Common/src/main/java/mage/remote/SessionImpl.java +++ b/Mage.Common/src/main/java/mage/remote/SessionImpl.java @@ -518,7 +518,7 @@ public class SessionImpl implements Session { } try { - if (callbackClient.isConnected()) { + if (callbackClient != null && callbackClient.isConnected()) { callbackClient.removeListener(callbackHandler); callbackClient.disconnect(); } diff --git a/Mage.Sets/src/mage/cards/a/AminatouTheFateshifter.java b/Mage.Sets/src/mage/cards/a/AminatouTheFateshifter.java index 54c6abcf700..9125e8fc0f8 100644 --- a/Mage.Sets/src/mage/cards/a/AminatouTheFateshifter.java +++ b/Mage.Sets/src/mage/cards/a/AminatouTheFateshifter.java @@ -30,7 +30,6 @@ import mage.target.targetpointer.FixedTarget; import java.util.UUID; /** - * * @author Colin Redman */ public class AminatouTheFateshifter extends CardImpl { @@ -53,9 +52,9 @@ public class AminatouTheFateshifter extends CardImpl { Ability ability = new LoyaltyAbility(new AminatouPlusEffect(), +1); this.addAbility(ability); - // -1: Exile another target permanent you own, then return it to the battlefield under your control. + // −1: Exile another target permanent you own, then return it to the battlefield under your control. ability = new LoyaltyAbility(new ExileTargetForSourceEffect(), -1); - ability.addEffect(new ReturnToBattlefieldUnderYourControlTargetEffect(true)); + ability.addEffect(new ReturnToBattlefieldUnderYourControlTargetEffect(false)); ability.addTarget(new TargetPermanent(filter)); this.addAbility(ability); From e9c68d2a5cc1a9ed2afe97ea2f1953fd0663ded7 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 21 Aug 2021 15:02:42 +0400 Subject: [PATCH 03/17] Test framework improves (related to #7896): * added real time command to check card's counters (example: suspended cards with Time counters, see checkCardCounters); * added target type support: TargetPermanentOrSuspendedCard; * improves error logs for miss modes and unsupported target types; --- .../java/org/mage/test/player/TestPlayer.java | 94 +++++++++++++++---- .../base/impl/CardTestPlayerAPIImpl.java | 5 + .../TargetPermanentOrSuspendedCard.java | 31 +----- 3 files changed, 80 insertions(+), 50 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index d702521448a..4411373ea3c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -342,7 +342,7 @@ public class TestPlayer implements Player { for (Player player : game.getPlayers().values()) { if (player.getName().equals(target)) { if (ability.getTargets().isEmpty()) { - throw new UnsupportedOperationException("Ability has no targets, but there is a player target set - " + ability.toString()); + throw new UnsupportedOperationException("Ability has no targets, but there is a player target set - " + ability); } if (ability.getTargets().get(0) instanceof TargetAmount) { return true; // targetAmount have to be set by setTargetAmount in the test script @@ -461,10 +461,10 @@ public class TestPlayer implements Player { selectedMode = ability.getModes().getMode(); } if (selectedMode == null) { - throw new UnsupportedOperationException("Mode not available for " + ability.toString()); + throw new UnsupportedOperationException("Mode not available for " + ability); } if (selectedMode.getTargets().isEmpty()) { - throw new AssertionError("Ability has no targets. " + ability.toString()); + throw new AssertionError("Ability has no targets. " + ability); } if (index >= selectedMode.getTargets().size()) { break; // this can happen if targets should be set but can't be used because of hexproof e.g. @@ -821,6 +821,13 @@ public class TestPlayer implements Player { wasProccessed = true; } + // check card counters: card name, counter type, count + if (params[0].equals(CHECK_COMMAND_CARD_COUNTERS) && params.length == 4) { + assertCardCounters(action, game, computerPlayer, params[1], CounterType.findByName(params[2]), Integer.parseInt(params[3])); + actions.remove(action); + wasProccessed = true; + } + // check exile count: card name, count if (params[0].equals(CHECK_COMMAND_EXILE_COUNT) && params.length == 3) { assertExileCount(action, game, params[1], Integer.parseInt(params[2])); @@ -1365,6 +1372,25 @@ public class TestPlayer implements Player { Assert.assertEquals(action.getActionName() + " - permanent " + permanentName + " must have " + count + " " + counterType.toString(), count, foundCount); } + private void assertCardCounters(PlayerAction action, Game game, Player player, String cardName, CounterType counterType, int count) { + int foundCount = 0; + + Set allCards = new HashSet<>(); + + // collect possible cards from visible zones except library + allCards.addAll(player.getHand().getCards(game)); + allCards.addAll(player.getGraveyard().getCards(game)); + allCards.addAll(game.getExile().getAllCards(game)); + + for (Card card : allCards) { + if (hasObjectTargetNameOrAlias(card, cardName) && card.getOwnerId().equals(player.getId())) { + foundCount = card.getCounters(game).getCount(counterType); + } + } + + Assert.assertEquals(action.getActionName() + " - card " + cardName + " must have " + count + " " + counterType.toString(), count, foundCount); + } + private void assertExileCount(PlayerAction action, Game game, String permanentName, int count) { int foundCount = 0; for (Card card : game.getExile().getAllCards(game)) { @@ -1465,9 +1491,9 @@ public class TestPlayer implements Player { } if (mustHave) { - Assert.assertEquals(action.getActionName() + " - must contain colors [" + searchColors.toString() + "] but found only [" + cardColor.toString() + "]", 0, colorsDontHave.size()); + Assert.assertEquals(action.getActionName() + " - must contain colors [" + searchColors + "] but found only [" + cardColor.toString() + "]", 0, colorsDontHave.size()); } else { - Assert.assertEquals(action.getActionName() + " - must not contain colors [" + searchColors.toString() + "] but found [" + cardColor.toString() + "]", 0, colorsHave.size()); + Assert.assertEquals(action.getActionName() + " - must not contain colors [" + searchColors + "] but found [" + cardColor.toString() + "]", 0, colorsHave.size()); } } @@ -1558,7 +1584,7 @@ public class TestPlayer implements Player { Integer normal = player.getManaPool().getMana().get(manaType); Integer conditional = player.getManaPool().getConditionalMana().stream().mapToInt(a -> a.get(manaType)).sum(); // calcs FULL conditional mana, not real conditions Integer current = normal + conditional; - Assert.assertEquals(action.getActionName() + " - mana pool must contain [" + amount.toString() + " " + manaType.toString() + "], but found [" + current.toString() + "]", amount, current); + Assert.assertEquals(action.getActionName() + " - mana pool must contain [" + amount.toString() + " " + manaType + "], but found [" + current + "]", amount, current); } private void assertManaPool(PlayerAction action, Game game, Player player, String colors, Integer amount) { @@ -1952,7 +1978,18 @@ public class TestPlayer implements Player { return null; } - this.chooseStrictModeFailed("mode", game, getInfo(source, game)); + StringBuilder modesInfo = new StringBuilder(); + modesInfo.append("\nAvailable modes:"); + int i = 1; + for (Mode mode : modes.getAvailableModes(source, game)) { + if (modesInfo.length() > 0) { + modesInfo.append("\n"); + } + modesInfo.append(String.format("%d: %s", i, mode.getEffects().getText(mode))); + i++; + } + + this.chooseStrictModeFailed("mode", game, getInfo(source, game) + modesInfo); return computerPlayer.chooseMode(modes, source, game); } @@ -2298,7 +2335,8 @@ public class TestPlayer implements Player { || (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) || (target.getOriginalTarget() instanceof TargetAnyTarget) || (target.getOriginalTarget() instanceof TargetCreatureOrPlayer) - || (target.getOriginalTarget() instanceof TargetDefender)) { + || (target.getOriginalTarget() instanceof TargetDefender) + || (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard)) { for (String targetDefinition : targets) { if (targetDefinition.startsWith("targetPlayer=")) { continue; @@ -2330,6 +2368,9 @@ public class TestPlayer implements Player { if (filter instanceof FilterPlaneswalkerOrPlayer) { filter = ((FilterPlaneswalkerOrPlayer) filter).getFilterPermanent(); } + if (filter instanceof FilterPermanentOrSuspendedCard) { + filter = ((FilterPermanentOrSuspendedCard) filter).getPermanentFilter(); + } for (Permanent permanent : game.getBattlefield().getActivePermanents((FilterPermanent) filter, abilityControllerId, sourceId, game)) { if (hasObjectTargetNameOrAlias(permanent, targetName) || (permanent.getName() + '-' + permanent.getExpansionSetCode()).equals(targetName)) { // TODO: remove exp code search? if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.getTargets().contains(permanent.getId())) { @@ -2375,14 +2416,26 @@ public class TestPlayer implements Player { } // card in exile - if (target.getOriginalTarget() instanceof TargetCardInExile) { - TargetCardInExile targetFull = (TargetCardInExile) target.getOriginalTarget(); + if (target.getOriginalTarget() instanceof TargetCardInExile + || target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) { + + FilterCard filterCard = null; + if (target.getOriginalTarget().getFilter() instanceof FilterCard) { + filterCard = (FilterCard) target.getOriginalTarget().getFilter(); + } else if (target.getOriginalTarget().getFilter() instanceof FilterPermanentOrSuspendedCard) { + filterCard = ((FilterPermanentOrSuspendedCard) target.getOriginalTarget().getFilter()).getCardFilter(); + } + if (filterCard == null) { + Assert.fail("Unsupported exile target filter in TestPlayer: " + + target.getOriginalTarget().getClass().getCanonicalName()); + } + for (String targetDefinition : targets) { checkTargetDefinitionMarksSupport(target, targetDefinition, "^"); String[] targetList = targetDefinition.split("\\^"); boolean targetFound = false; for (String targetName : targetList) { - for (Card card : game.getExile().getCards(targetFull.getFilter(), game)) { + for (Card card : game.getExile().getCards(filterCard, game)) { if (hasObjectTargetNameOrAlias(card, targetName) || (card.getName() + '-' + card.getExpansionSetCode()).equals(targetName)) { // TODO: remove set code search? if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.getTargets().contains(card.getId())) { target.addTarget(card.getId(), source, game); @@ -2508,16 +2561,17 @@ public class TestPlayer implements Player { String message; if (source != null) { - message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used in [" - + "card " + source.getSourceObject(game) - + " -> ability " + source.getClass().getSimpleName() + " (" + source.getRule().substring(0, Math.min(20, source.getRule().length()) - 1) + "..." + ")" - + " -> target " + target.getClass().getSimpleName() + " (" + target.getMessage() + ")" - + "]"; + message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used" + + "\nCard: " + source.getSourceObject(game) + + "\nAbility: " + source.getClass().getSimpleName() + " (" + source.getRule() + ")" + + "\nTarget: " + target.getClass().getSimpleName() + " (" + target.getMessage() + ")" + + "\nYou must implement target class support in TestPlayer or setup good targets"; } else { - message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used in [" - + "card XXX" - + " -> target " + target.getClass().getSimpleName() + " (" + target.getMessage() + ")" - + "]"; + message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used" + + "\nCard: unknown source" + + "\nAbility: unknown source" + + "\nTarget: " + target.getClass().getSimpleName() + " (" + target.getMessage() + ")" + + "\nYou must implement target class support in TestPlayer or setup good targets"; } Assert.fail(message); } 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 1ed8cad43ce..f9e9162d37e 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 @@ -97,6 +97,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public static final String CHECK_COMMAND_PERMANENT_COUNT = "PERMANENT_COUNT"; public static final String CHECK_COMMAND_PERMANENT_TAPPED = "PERMANENT_TAPPED"; public static final String CHECK_COMMAND_PERMANENT_COUNTERS = "PERMANENT_COUNTERS"; + public static final String CHECK_COMMAND_CARD_COUNTERS = "CARD_COUNTERS"; public static final String CHECK_COMMAND_EXILE_COUNT = "EXILE_COUNT"; public static final String CHECK_COMMAND_GRAVEYARD_COUNT = "GRAVEYARD_COUNT"; public static final String CHECK_COMMAND_LIBRARY_COUNT = "LIBRARY_COUNT"; @@ -457,6 +458,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement check(checkName, turnNum, step, player, CHECK_COMMAND_PERMANENT_COUNTERS, permanentName, counterType.toString(), count.toString()); } + public void checkCardCounters(String checkName, int turnNum, PhaseStep step, TestPlayer player, String cardName, CounterType counterType, Integer count) { + check(checkName, turnNum, step, player, CHECK_COMMAND_CARD_COUNTERS, cardName, counterType.toString(), count.toString()); + } + public void checkExileCount(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, Integer count) { //Assert.assertNotEquals("", permanentName); check(checkName, turnNum, step, player, CHECK_COMMAND_EXILE_COUNT, permanentName, count.toString()); diff --git a/Mage/src/main/java/mage/target/common/TargetPermanentOrSuspendedCard.java b/Mage/src/main/java/mage/target/common/TargetPermanentOrSuspendedCard.java index 186d5bc6b34..64d399238a1 100644 --- a/Mage/src/main/java/mage/target/common/TargetPermanentOrSuspendedCard.java +++ b/Mage/src/main/java/mage/target/common/TargetPermanentOrSuspendedCard.java @@ -1,32 +1,3 @@ -/* - * - * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * The views and conclusions contained in the software and documentation are those of the - * authors and should not be interpreted as representing official policies, either expressed - * or implied, of BetaSteward_at_googlemail.com. - * - */ package mage.target.common; import mage.MageObject; @@ -154,7 +125,7 @@ public class TargetPermanentOrSuspendedCard extends TargetImpl { @Override public String getTargetedName(Game game) { - StringBuilder sb = new StringBuilder(""); + StringBuilder sb = new StringBuilder(); for (UUID targetId : this.getTargets()) { Permanent permanent = game.getPermanent(targetId); if (permanent != null) { From 7dcdc12c0fd42ff90b2324ebbd365052611cc751 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 21 Aug 2021 15:06:11 +0400 Subject: [PATCH 04/17] * Target permanent or suspended card - added AI support, fixed rollback errors in AI games (example: Shivan Sand-Mage, see #7896); --- .../java/mage/player/ai/ComputerPlayer.java | 100 ++++++++++++--- .../cards/single/tsr/ShivanSandMageTest.java | 121 ++++++++++++++++++ .../java/org/mage/test/player/TestPlayer.java | 10 +- 3 files changed, 210 insertions(+), 21 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tsr/ShivanSandMageTest.java diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index f7c3ed93023..a292a99e72e 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -130,7 +130,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game, Map options) { if (log.isDebugEnabled()) { - log.debug("chooseTarget: " + outcome.toString() + ':' + target.toString()); + log.debug("choose: " + outcome.toString() + ':' + target.toString()); } // controller hints: @@ -205,21 +205,30 @@ public class ComputerPlayer extends PlayerImpl implements Player { } if (target.getOriginalTarget() instanceof TargetPermanent) { - TargetPermanent origTarget = (TargetPermanent) target.getOriginalTarget(); + + FilterPermanent filter = null; + if (target.getOriginalTarget().getFilter() instanceof FilterPermanent) { + filter = (FilterPermanent) target.getOriginalTarget().getFilter(); + } + if (filter == null) { + throw new IllegalStateException("Unsupported permanent filter in computer's choose method: " + + target.getOriginalTarget().getClass().getCanonicalName()); + } + List targets; if (outcome.isCanTargetAll()) { - targets = threats(null, sourceId, origTarget.getFilter(), game, target.getTargets()); + targets = threats(null, sourceId, filter, game, target.getTargets()); } else { if (outcome.isGood()) { - targets = threats(abilityControllerId, sourceId, origTarget.getFilter(), game, target.getTargets()); + targets = threats(abilityControllerId, sourceId, filter, game, target.getTargets()); } else { - targets = threats(randomOpponentId, sourceId, origTarget.getFilter(), game, target.getTargets()); + targets = threats(randomOpponentId, sourceId, filter, game, target.getTargets()); } if (targets.isEmpty() && target.isRequired()) { if (!outcome.isGood()) { - targets = threats(abilityControllerId, sourceId, origTarget.getFilter(), game, target.getTargets()); + targets = threats(abilityControllerId, sourceId, filter, game, target.getTargets()); } else { - targets = threats(randomOpponentId, sourceId, origTarget.getFilter(), game, target.getTargets()); + targets = threats(randomOpponentId, sourceId, filter, game, target.getTargets()); } } } @@ -429,8 +438,17 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (target.getOriginalTarget() instanceof TargetCardInExile) { List alreadyTargeted = target.getTargets(); - TargetCard originalTarget = (TargetCard) target.getOriginalTarget(); - List cards = game.getExile().getCards(originalTarget.getFilter(), game); + + FilterCard filter = null; + if (target.getOriginalTarget().getFilter() instanceof FilterCard) { + filter = (FilterCard) target.getOriginalTarget().getFilter(); + } + if (filter == null) { + throw new IllegalStateException("Unsupported exile target filter in computer's choose method: " + + target.getOriginalTarget().getClass().getCanonicalName()); + } + + List cards = game.getExile().getCards(filter, game); while (!cards.isEmpty()) { Card card = pickTarget(abilityControllerId, cards, outcome, target, null, game); if (card != null && alreadyTargeted != null && !alreadyTargeted.contains(card.getId())) { @@ -461,10 +479,23 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (!required) { return false; } - throw new IllegalStateException("TargetSource wasn't handled. class: " + target.getClass().toString()); + throw new IllegalStateException("TargetSource wasn't handled in computer's choose method: " + target.getClass().getCanonicalName()); } - throw new IllegalStateException("Target wasn't handled. class: " + target.getClass().toString()); + if (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) { + Cards cards = new CardsImpl(possibleTargets); + List possibleCards = new ArrayList<>(cards.getCards(game)); + while (!target.isChosen() && !possibleCards.isEmpty()) { + Card pick = pickTarget(abilityControllerId, possibleCards, outcome, target, null, game); + if (pick != null) { + target.addTarget(pick.getId(), null, game); + possibleCards.remove(pick); + } + } + return target.isChosen(); + } + + throw new IllegalStateException("Target wasn't handled in computer's choose method: " + target.getClass().getCanonicalName()); } //end of choose method @Override @@ -578,8 +609,19 @@ public class ComputerPlayer extends PlayerImpl implements Player { // TODO: implemented findBestPlayerTargets // TODO: add findBest*Targets for all target types if (target.getOriginalTarget() instanceof TargetPermanent) { - TargetPermanent origTarget = (TargetPermanent) target.getOriginalTarget(); - findBestPermanentTargets(outcome, abilityControllerId, sourceId, origTarget.getFilter(), + + FilterPermanent filter = null; + if (target.getOriginalTarget().getFilter() instanceof FilterPermanent) { + filter = (FilterPermanent) target.getOriginalTarget().getFilter(); + } else if (target.getOriginalTarget().getFilter() instanceof FilterPermanentOrSuspendedCard) { + filter = ((FilterPermanentOrSuspendedCard) target.getOriginalTarget().getFilter()).getPermanentFilter(); + } + if (filter == null) { + throw new IllegalStateException("Unsupported permanent filter in computer's chooseTarget method: " + + target.getOriginalTarget().getClass().getCanonicalName()); + } + + findBestPermanentTargets(outcome, abilityControllerId, sourceId, filter, game, target, goodList, badList, allList); // use good list all the time and add maximum targets @@ -921,10 +963,20 @@ public class ComputerPlayer extends PlayerImpl implements Player { } if (target.getOriginalTarget() instanceof TargetCardInExile) { + + FilterCard filter = null; + if (target.getOriginalTarget().getFilter() instanceof FilterCard) { + filter = (FilterCard) target.getOriginalTarget().getFilter(); + } + if (filter == null) { + throw new IllegalStateException("Unsupported exile target filter in computer's chooseTarget method: " + + target.getOriginalTarget().getClass().getCanonicalName()); + } + List cards = new ArrayList<>(); for (UUID uuid : target.possibleTargets(source.getSourceId(), source.getControllerId(), game)) { Card card = game.getCard(uuid); - if (card != null) { + if (card != null && game.getState().getZone(card.getId()) == Zone.EXILED) { cards.add(card); } } @@ -968,7 +1020,20 @@ public class ComputerPlayer extends PlayerImpl implements Player { } } - throw new IllegalStateException("Target wasn't handled. class:" + target.getClass().toString()); + if (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) { + Cards cards = new CardsImpl(possibleTargets); + List possibleCards = new ArrayList<>(cards.getCards(game)); + while (!target.isChosen() && !possibleCards.isEmpty()) { + Card pick = pickTarget(abilityControllerId, possibleCards, outcome, target, source, game); + if (pick != null) { + target.addTarget(pick.getId(), source, game); + possibleCards.remove(pick); + } + } + return target.isChosen(); + } + + throw new IllegalStateException("Target wasn't handled in computer's chooseTarget method: " + target.getClass().getCanonicalName()); } //end of chooseTarget method protected Card pickTarget(UUID abilityControllerId, List cards, Outcome outcome, Target target, Ability source, Game game) { @@ -1996,7 +2061,10 @@ public class ComputerPlayer extends PlayerImpl implements Player { // mode was already set by the AI return modes.getMode(); } - //TODO: improve this; + + // spell modes simulated by AI, see addModeOptions + // trigger modes chooses here + // TODO: add AI support to select best modes, current code uses first valid mode AvailableMode: for (Mode mode : modes.getAvailableModes(source, game)) { for (UUID selectedModeId : modes.getSelectedModes()) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tsr/ShivanSandMageTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tsr/ShivanSandMageTest.java new file mode 100644 index 00000000000..3256fac646b --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tsr/ShivanSandMageTest.java @@ -0,0 +1,121 @@ +package org.mage.test.cards.single.tsr; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +/** + * @author JayDi85 + */ +public class ShivanSandMageTest extends CardTestPlayerBaseWithAIHelps { + + @Test + public void test_ModeOne_Manual() { + // When Shivan Sand-Mage enters the battlefield, choose one — + // • Remove two time counters from target permanent or suspended card. + // • Put two time counters on target permanent with a time counter on it or suspended card. + addCard(Zone.HAND, playerA, "Shivan Sand-Mage", 1); // {2}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shivan Sand-Mage"); + setModeChoice(playerA, "1"); + addTarget(playerA, "Grizzly Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Shivan Sand-Mage", 1); + } + + @Test + public void test_ModeOne_AI() { + // When Shivan Sand-Mage enters the battlefield, choose one — + // • Remove two time counters from target permanent or suspended card. + // • Put two time counters on target permanent with a time counter on it or suspended card. + addCard(Zone.HAND, playerA, "Shivan Sand-Mage", 1); // {2}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); + + // AI must play card + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Shivan Sand-Mage", 1); + } + + @Test + public void test_ModeTwo_Manual() { + // When Shivan Sand-Mage enters the battlefield, choose one — + // • Remove two time counters from target permanent or suspended card. + // • Put two time counters on target permanent with a time counter on it or suspended card. + addCard(Zone.HAND, playerA, "Shivan Sand-Mage", 1); // {2}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + // + // Suspend 3—{1}{W} + addCard(Zone.HAND, playerA, "Duskrider Peregrine", 1); // {5}{W} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + // prepare suspended card + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {W}", 2); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Suspend 3"); + checkExileCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Duskrider Peregrine", 1); + checkCardCounters("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Duskrider Peregrine", CounterType.TIME, 3); + + // add +2 time counters + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shivan Sand-Mage"); + setModeChoice(playerA, "2"); + addTarget(playerA, "Duskrider Peregrine"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkCardCounters("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Duskrider Peregrine", CounterType.TIME, 3 + 2); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Shivan Sand-Mage", 1); + } + + @Test + public void test_ModeTwo_AI() { + // When Shivan Sand-Mage enters the battlefield, choose one — + // • Remove two time counters from target permanent or suspended card. + // • Put two time counters on target permanent with a time counter on it or suspended card. + addCard(Zone.HAND, playerA, "Shivan Sand-Mage", 1); // {2}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + // + // Suspend 3—{1}{W} + addCard(Zone.HAND, playerA, "Duskrider Peregrine", 1); // {5}{W} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + // prepare suspended card + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {W}", 2); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Suspend 3"); + checkExileCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Duskrider Peregrine", 1); + checkCardCounters("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Duskrider Peregrine", CounterType.TIME, 3); + + // must add +2 time counters + // due to AI limitation with ETB's modes - it checks only target support + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shivan Sand-Mage"); + setModeChoice(playerA, "2"); + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkCardCounters("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Duskrider Peregrine", CounterType.TIME, 3 + 2); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Shivan Sand-Mage", 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 4411373ea3c..be12fae6908 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -2419,13 +2419,13 @@ public class TestPlayer implements Player { if (target.getOriginalTarget() instanceof TargetCardInExile || target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) { - FilterCard filterCard = null; + FilterCard filter = null; if (target.getOriginalTarget().getFilter() instanceof FilterCard) { - filterCard = (FilterCard) target.getOriginalTarget().getFilter(); + filter = (FilterCard) target.getOriginalTarget().getFilter(); } else if (target.getOriginalTarget().getFilter() instanceof FilterPermanentOrSuspendedCard) { - filterCard = ((FilterPermanentOrSuspendedCard) target.getOriginalTarget().getFilter()).getCardFilter(); + filter = ((FilterPermanentOrSuspendedCard) target.getOriginalTarget().getFilter()).getCardFilter(); } - if (filterCard == null) { + if (filter == null) { Assert.fail("Unsupported exile target filter in TestPlayer: " + target.getOriginalTarget().getClass().getCanonicalName()); } @@ -2435,7 +2435,7 @@ public class TestPlayer implements Player { String[] targetList = targetDefinition.split("\\^"); boolean targetFound = false; for (String targetName : targetList) { - for (Card card : game.getExile().getCards(filterCard, game)) { + for (Card card : game.getExile().getCards(filter, game)) { if (hasObjectTargetNameOrAlias(card, targetName) || (card.getName() + '-' + card.getExpansionSetCode()).equals(targetName)) { // TODO: remove set code search? if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.getTargets().contains(card.getId())) { target.addTarget(card.getId(), source, game); From e25d2878640e395d917e7e2690b343c87600a73d Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 21 Aug 2021 17:22:06 +0400 Subject: [PATCH 05/17] Test framework: added test fails on errors in AI's simulated games; --- .../src/mage/player/ai/ComputerPlayer6.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index 2a177672685..9b13c4809ae 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -446,8 +446,14 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { logger.info("simulating - timed out"); task.cancel(true); } catch (ExecutionException e) { + // exception error in simulated game e.printStackTrace(); task.cancel(true); + // real games: must catch + // unit tests: must raise again for test fail + if (this.isTestsMode()) { + throw new IllegalStateException("One of the simulated games raise the error: " + e.getCause()); + } } catch (InterruptedException e) { e.printStackTrace(); task.cancel(true); From a76a006065e800aee485e7dda834f3524c0cbaba Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 21 Aug 2021 17:50:36 +0400 Subject: [PATCH 06/17] [AFC] fixed Bag of Devouring - NPE on rules/database generation; --- Mage.Sets/src/mage/cards/b/BagOfDevouring.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/b/BagOfDevouring.java b/Mage.Sets/src/mage/cards/b/BagOfDevouring.java index 35ae6ebd417..0c9ebe17a67 100644 --- a/Mage.Sets/src/mage/cards/b/BagOfDevouring.java +++ b/Mage.Sets/src/mage/cards/b/BagOfDevouring.java @@ -59,7 +59,7 @@ public final class BagOfDevouring extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{B}"); // Whenever you sacrifice another nontoken artifact or creature, exile it. - this.addAbility(new SacrificePermanentTriggeredAbility(new ExileTargetForSourceEffect(), filter, true)); + this.addAbility(new SacrificePermanentTriggeredAbility(new ExileTargetForSourceEffect().setText("exile it"), filter, true)); // {2}, {T}, Sacrifice another artifact or creature: Draw a card. Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(1), new GenericManaCost(2)); From 14838e670a6719fa531b2ae7fe15ed442b97c194 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 21 Aug 2021 17:55:44 +0400 Subject: [PATCH 07/17] Merge fix --- .../src/main/java/mage/player/ai/ComputerPlayer.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index a292a99e72e..4c462dbbeb4 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -613,8 +613,6 @@ public class ComputerPlayer extends PlayerImpl implements Player { FilterPermanent filter = null; if (target.getOriginalTarget().getFilter() instanceof FilterPermanent) { filter = (FilterPermanent) target.getOriginalTarget().getFilter(); - } else if (target.getOriginalTarget().getFilter() instanceof FilterPermanentOrSuspendedCard) { - filter = ((FilterPermanentOrSuspendedCard) target.getOriginalTarget().getFilter()).getPermanentFilter(); } if (filter == null) { throw new IllegalStateException("Unsupported permanent filter in computer's chooseTarget method: " From b9550a7387e5d1fd353768e62e8eaacf5e142241 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 21 Aug 2021 08:56:51 -0400 Subject: [PATCH 08/17] made a util function more generic because why not --- Mage.Sets/src/mage/cards/m/MaddeningHex.java | 2 +- Mage/src/main/java/mage/players/PlayerImpl.java | 2 +- Mage/src/main/java/mage/util/RandomUtil.java | 11 +++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Mage.Sets/src/mage/cards/m/MaddeningHex.java b/Mage.Sets/src/mage/cards/m/MaddeningHex.java index 9d19bcca303..90ec227dd91 100644 --- a/Mage.Sets/src/mage/cards/m/MaddeningHex.java +++ b/Mage.Sets/src/mage/cards/m/MaddeningHex.java @@ -132,7 +132,7 @@ class MaddeningHexEffect extends OneShotEffect { opponents.remove(player.getId()); } if (!opponents.isEmpty()) { - permanent.attachTo(RandomUtil.randomFromSet(opponents), source, game); + permanent.attachTo(RandomUtil.randomFromCollection(opponents), source, game); } return true; } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 4dc9bbc5630..6e1e60d2b3a 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -2716,7 +2716,7 @@ public abstract class PlayerImpl implements Player, Serializable { .stream() .filter(card -> filter.match(card, source.getSourceId(), getId(), game)) .collect(Collectors.toSet()); - Card card = RandomUtil.randomFromSet(cards); + Card card = RandomUtil.randomFromCollection(cards); if (card == null) { return false; } diff --git a/Mage/src/main/java/mage/util/RandomUtil.java b/Mage/src/main/java/mage/util/RandomUtil.java index eebff33234c..5f2458c4dcc 100644 --- a/Mage/src/main/java/mage/util/RandomUtil.java +++ b/Mage/src/main/java/mage/util/RandomUtil.java @@ -1,16 +1,15 @@ package mage.util; import java.awt.*; -import java.io.Serializable; +import java.util.Collection; import java.util.Random; -import java.util.Set; /** * Created by IGOUDT on 5-9-2016. */ public final class RandomUtil { - private static Random random = new Random(); // thread safe with seed support + private static final Random random = new Random(); // thread safe with seed support private RandomUtil() { } @@ -43,15 +42,15 @@ public final class RandomUtil { random.setSeed(newSeed); } - public static T randomFromSet(Set collection) { + public static T randomFromCollection(Collection collection) { if (collection.size() < 2) { return collection.stream().findFirst().orElse(null); } int rand = nextInt(collection.size()); int count = 0; - for (T currentId : collection) { + for (T current : collection) { if (count == rand) { - return currentId; + return current; } count++; } From 898f8ca287e07462686f849fdc13acd7285ae9cf Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 21 Aug 2021 09:12:19 -0400 Subject: [PATCH 09/17] [AFC] Implemented Klauth's Will --- Mage.Sets/src/mage/cards/k/KlauthsWill.java | 78 +++++++++++++++++++ .../mage/sets/ForgottenRealmsCommander.java | 1 + 2 files changed, 79 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/k/KlauthsWill.java diff --git a/Mage.Sets/src/mage/cards/k/KlauthsWill.java b/Mage.Sets/src/mage/cards/k/KlauthsWill.java new file mode 100644 index 00000000000..882db834cd7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KlauthsWill.java @@ -0,0 +1,78 @@ +package mage.cards.k; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.condition.common.ControlACommanderCondition; +import mage.abilities.dynamicvalue.common.ManacostVariableValue; +import mage.abilities.effects.common.DamageAllEffect; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.game.Game; +import mage.target.TargetPermanent; +import mage.target.targetadjustment.TargetAdjuster; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class KlauthsWill extends CardImpl { + + private static final FilterPermanent filter = new FilterCreaturePermanent("creature without flying"); + + static { + filter.add(Predicates.not(new AbilityPredicate(FlyingAbility.class))); + } + + public KlauthsWill(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{X}{R}{R}{G}"); + + // Choose one. If you control a commander as you cast this spell, you may choose both. + this.getSpellAbility().getModes().setChooseText( + "Choose one. If you control a commander as you cast this spell, you may choose both." + ); + this.getSpellAbility().getModes().setMoreCondition(ControlACommanderCondition.instance); + + // • Breathe Flame — Klauth's Will deals X damage to each creature without flying. + this.getSpellAbility().addEffect(new DamageAllEffect(ManacostVariableValue.REGULAR, filter)); + this.getSpellAbility().withFirstModeFlavorWord("Breathe Flame"); + + // • Smash Relics — Destroy up to X target artifacts and/or enchantments. + this.getSpellAbility().addMode(new Mode( + new DestroyTargetEffect().setText("destroy up to X target artifacts and/or enchantments") + ).withFlavorWord("Smash Relics")); + this.getSpellAbility().setTargetAdjuster(KlauthsWillAdjuster.instance); + } + + private KlauthsWill(final KlauthsWill card) { + super(card); + } + + @Override + public KlauthsWill copy() { + return new KlauthsWill(this); + } +} + +enum KlauthsWillAdjuster implements TargetAdjuster { + instance; + + @Override + public void adjustTargets(Ability ability, Game game) { + if (ability.getEffects().stream().anyMatch(DestroyTargetEffect.class::isInstance)) { + ability.getTargets().clear(); + ability.addTarget(new TargetPermanent( + 0, ability.getManaCostsToPay().getX(), + StaticFilters.FILTER_PERMANENT_ARTIFACT_OR_ENCHANTMENT + )); + } + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java index 72b707ff026..c662f18d6cd 100644 --- a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java +++ b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java @@ -139,6 +139,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Karmic Guide", 68, Rarity.RARE, mage.cards.k.KarmicGuide.class)); cards.add(new SetCardInfo("Kenrith's Transformation", 162, Rarity.UNCOMMON, mage.cards.k.KenrithsTransformation.class)); cards.add(new SetCardInfo("Kindred Summons", 163, Rarity.RARE, mage.cards.k.KindredSummons.class)); + cards.add(new SetCardInfo("Klauth's Will", 51, Rarity.RARE, mage.cards.k.KlauthsWill.class)); cards.add(new SetCardInfo("Klauth, Unrivaled Ancient", 50, Rarity.MYTHIC, mage.cards.k.KlauthUnrivaledAncient.class)); cards.add(new SetCardInfo("Knight of Autumn", 187, Rarity.RARE, mage.cards.k.KnightOfAutumn.class)); cards.add(new SetCardInfo("Light Up the Stage", 131, Rarity.UNCOMMON, mage.cards.l.LightUpTheStage.class)); From d20b15c7b6ec3f68150fb6549dfb6b31be1ca20c Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 21 Aug 2021 11:20:16 -0400 Subject: [PATCH 10/17] [AFC] Implemented Hellish Rebuke --- Mage.Sets/src/mage/cards/h/HellishRebuke.java | 123 ++++++++++++++++++ .../mage/sets/ForgottenRealmsCommander.java | 1 + .../watchers/common/SpellsCastWatcher.java | 16 +++ 3 files changed, 140 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HellishRebuke.java diff --git a/Mage.Sets/src/mage/cards/h/HellishRebuke.java b/Mage.Sets/src/mage/cards/h/HellishRebuke.java new file mode 100644 index 00000000000..8d3674604b0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HellishRebuke.java @@ -0,0 +1,123 @@ +package mage.cards.h; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; +import mage.abilities.effects.common.SacrificeSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilityAllEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.watchers.common.SpellsCastWatcher; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class HellishRebuke extends CardImpl { + + public HellishRebuke(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{B}"); + + // Until end of turn, permanents your opponents control gain "When this permanent deals damage to the player who cast Hellish Rebuke, sacrifice this permanent. You lose 2 life." + this.getSpellAbility().addEffect(new HellishRebukeEffect()); + this.getSpellAbility().addWatcher(new SpellsCastWatcher()); + } + + private HellishRebuke(final HellishRebuke card) { + super(card); + } + + @Override + public HellishRebuke copy() { + return new HellishRebuke(this); + } +} + +class HellishRebukeEffect extends OneShotEffect { + + HellishRebukeEffect() { + super(Outcome.Benefit); + staticText = "until end of turn, permanents your opponents control gain " + + "\"When this permanent deals damage to the player who cast {this}, " + + "sacrifice this permanent. You lose 2 life.\""; + } + + private HellishRebukeEffect(final HellishRebukeEffect effect) { + super(effect); + } + + @Override + public HellishRebukeEffect copy() { + return new HellishRebukeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + MageObject mageObject = source.getSourceObject(game); + game.addEffect(new GainAbilityAllEffect( + new HellishRebukeTriggeredAbility(source, game), + Duration.EndOfTurn, StaticFilters.FILTER_OPPONENTS_PERMANENT + ), source); + return true; + } +} + +class HellishRebukeTriggeredAbility extends TriggeredAbilityImpl { + + private final String sourceName; + private final UUID casterId; + + HellishRebukeTriggeredAbility(Ability source, Game game) { + super(Zone.BATTLEFIELD, new SacrificeSourceEffect()); + this.addEffect(new LoseLifeSourceControllerEffect(2)); + this.sourceName = getSourceName(source, game); + this.casterId = getCasterId(source, game); + } + + private HellishRebukeTriggeredAbility(final HellishRebukeTriggeredAbility ability) { + super(ability); + this.sourceName = ability.sourceName; + this.casterId = ability.casterId; + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_PLAYER; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return event.getSourceId().equals(getSourceId()) && event.getPlayerId().equals(casterId); + } + + @Override + public HellishRebukeTriggeredAbility copy() { + return new HellishRebukeTriggeredAbility(this); + } + + @Override + public String getRule() { + return "When this permanent deals damage to the player who cast " + + sourceName + ", sacrifice this permanent. You lose 2 life."; + } + + private static final String getSourceName(Ability source, Game game) { + MageObject object = source.getSourceObject(game); + return object != null ? object.getName() : "Hellish Rebuke"; + } + + private static final UUID getCasterId(Ability source, Game game) { + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + return watcher.getCasterId(source, game); + } +} diff --git a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java index c662f18d6cd..23ff352e9c0 100644 --- a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java +++ b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java @@ -125,6 +125,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Halimar Depths", 244, Rarity.COMMON, mage.cards.h.HalimarDepths.class)); cards.add(new SetCardInfo("Haven of the Spirit Dragon", 245, Rarity.RARE, mage.cards.h.HavenOfTheSpiritDragon.class)); cards.add(new SetCardInfo("Heirloom Blade", 208, Rarity.UNCOMMON, mage.cards.h.HeirloomBlade.class)); + cards.add(new SetCardInfo("Hellish Rebuke", 26, Rarity.RARE, mage.cards.h.HellishRebuke.class)); cards.add(new SetCardInfo("Heroic Intervention", 161, Rarity.RARE, mage.cards.h.HeroicIntervention.class)); cards.add(new SetCardInfo("Hex", 101, Rarity.RARE, mage.cards.h.Hex.class)); cards.add(new SetCardInfo("High Market", 246, Rarity.RARE, mage.cards.h.HighMarket.class)); diff --git a/Mage/src/main/java/mage/watchers/common/SpellsCastWatcher.java b/Mage/src/main/java/mage/watchers/common/SpellsCastWatcher.java index c4235d5b9b5..94f222340a4 100644 --- a/Mage/src/main/java/mage/watchers/common/SpellsCastWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/SpellsCastWatcher.java @@ -1,6 +1,8 @@ package mage.watchers.common; import mage.MageObject; +import mage.abilities.Ability; +import mage.cards.Card; import mage.constants.WatcherScope; import mage.constants.Zone; import mage.filter.StaticFilters; @@ -68,4 +70,18 @@ public class SpellsCastWatcher extends Watcher { public int getNumberOfNonCreatureSpells() { return nonCreatureSpells; } + + public UUID getCasterId(Ability source, Game game) { + for (Map.Entry> entry : spellsCast.entrySet()) { + if (entry.getValue() + .stream() + .map(Spell::getCard) + .map(Card::getMainCard) + .anyMatch(card -> card.getId().equals(source.getSourceId()) + && card.getZoneChangeCounter(game) == source.getSourceObjectZoneChangeCounter())) { + return entry.getKey(); + } + } + return null; + } } From f827924b8c3fd13251e3093c3b9b74852e4f66bf Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 21 Aug 2021 11:45:48 -0400 Subject: [PATCH 11/17] [AFC] Implemented Karazikar, the Eye Tyrant --- .../mage/cards/k/KarazikarTheEyeTyrant.java | 136 ++++++++++++++++++ .../mage/sets/ForgottenRealmsCommander.java | 1 + 2 files changed, 137 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/k/KarazikarTheEyeTyrant.java diff --git a/Mage.Sets/src/mage/cards/k/KarazikarTheEyeTyrant.java b/Mage.Sets/src/mage/cards/k/KarazikarTheEyeTyrant.java new file mode 100644 index 00000000000..8b6af086190 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KarazikarTheEyeTyrant.java @@ -0,0 +1,136 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.common.*; +import mage.abilities.effects.common.combat.GoadTargetEffect; +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.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.ControllerIdPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.targetpointer.FixedTarget; + +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class KarazikarTheEyeTyrant extends CardImpl { + + public KarazikarTheEyeTyrant(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}{R}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.BEHOLDER); + this.power = new MageInt(5); + this.toughness = new MageInt(5); + + // Whenever you attack a player, tap target creature that player controls and goad it. + this.addAbility(new KarazikarTheEyeTyrantFirstTriggeredAbility()); + + // Whenever an opponent attacks another one of your opponents, you and the attacking player each draw a card and lose 1 life. + this.addAbility(new KarazikarTheEyeTyrantSecondTriggeredAbility()); + } + + private KarazikarTheEyeTyrant(final KarazikarTheEyeTyrant card) { + super(card); + } + + @Override + public KarazikarTheEyeTyrant copy() { + return new KarazikarTheEyeTyrant(this); + } +} + +class KarazikarTheEyeTyrantFirstTriggeredAbility extends TriggeredAbilityImpl { + + KarazikarTheEyeTyrantFirstTriggeredAbility() { + super(Zone.BATTLEFIELD, new TapTargetEffect(), false); + this.addEffect(new GoadTargetEffect()); + } + + private KarazikarTheEyeTyrantFirstTriggeredAbility(final KarazikarTheEyeTyrantFirstTriggeredAbility ability) { + super(ability); + } + + @Override + public KarazikarTheEyeTyrantFirstTriggeredAbility copy() { + return new KarazikarTheEyeTyrantFirstTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DEFENDER_ATTACKED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!isControlledBy(event.getPlayerId())) { + return false; + } + Player player = game.getPlayer(event.getTargetId()); + if (player == null) { + return false; + } + FilterPermanent filter = new FilterCreaturePermanent("creature controlled by " + player.getName()); + filter.add(new ControllerIdPredicate(player.getId())); + this.getTargets().clear(); + this.addTarget(new TargetPermanent(filter)); + return true; + } + + @Override + public String getRule() { + return "Whenever you attack a player, tap target creature that player controls and goad it."; + } +} + +class KarazikarTheEyeTyrantSecondTriggeredAbility extends TriggeredAbilityImpl { + + KarazikarTheEyeTyrantSecondTriggeredAbility() { + super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false); + this.addEffect(new LoseLifeSourceControllerEffect(1)); + this.addEffect(new DrawCardTargetEffect(1)); + this.addEffect(new LoseLifeTargetEffect(1)); + } + + private KarazikarTheEyeTyrantSecondTriggeredAbility(final KarazikarTheEyeTyrantSecondTriggeredAbility ability) { + super(ability); + } + + @Override + public KarazikarTheEyeTyrantSecondTriggeredAbility copy() { + return new KarazikarTheEyeTyrantSecondTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DEFENDER_ATTACKED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Set opponents = game.getOpponents(getControllerId()); + if (!opponents.contains(event.getPlayerId()) || !opponents.contains(event.getTargetId())) { + return false; + } + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); + return true; + } + + @Override + public String getRule() { + return "Whenever an opponent attacks another one of your opponents, " + + "you and the attacking player each draw a card and lose 1 life."; + } +} diff --git a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java index 23ff352e9c0..26c4051ffa7 100644 --- a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java +++ b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java @@ -137,6 +137,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Imprisoned in the Moon", 85, Rarity.RARE, mage.cards.i.ImprisonedInTheMoon.class)); cards.add(new SetCardInfo("Indomitable Might", 40, Rarity.RARE, mage.cards.i.IndomitableMight.class)); cards.add(new SetCardInfo("Izzet Chemister", 130, Rarity.RARE, mage.cards.i.IzzetChemister.class)); + cards.add(new SetCardInfo("Karazikar, the Eye Tyrant", 49, Rarity.MYTHIC, mage.cards.k.KarazikarTheEyeTyrant.class)); cards.add(new SetCardInfo("Karmic Guide", 68, Rarity.RARE, mage.cards.k.KarmicGuide.class)); cards.add(new SetCardInfo("Kenrith's Transformation", 162, Rarity.UNCOMMON, mage.cards.k.KenrithsTransformation.class)); cards.add(new SetCardInfo("Kindred Summons", 163, Rarity.RARE, mage.cards.k.KindredSummons.class)); From 88484f5a1ea24a7f606bae65ef3d6f14dd2d89ba Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 21 Aug 2021 20:05:45 +0400 Subject: [PATCH 12/17] * GUI: fixed that Card Viewer can't open tokens list; --- Mage.Common/src/main/java/mage/view/CardView.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Mage.Common/src/main/java/mage/view/CardView.java b/Mage.Common/src/main/java/mage/view/CardView.java index 6f2f0f1265c..7462791f798 100644 --- a/Mage.Common/src/main/java/mage/view/CardView.java +++ b/Mage.Common/src/main/java/mage/view/CardView.java @@ -429,9 +429,11 @@ public class CardView extends SimpleCardView { this.cardIcons.add(FaceDownCardIcon.instance); } // commander - Player owner = game.getPlayer(game.getOwnerId(permanent)); - if (owner != null && game.isCommanderObject(owner, permanent)) { - this.cardIcons.add(CommanderCardIcon.instance); + if (game != null) { + Player owner = game.getPlayer(game.getOwnerId(permanent)); + if (owner != null && game.isCommanderObject(owner, permanent)) { + this.cardIcons.add(CommanderCardIcon.instance); + } } } else { if (card.isCopy()) { From 654ee7791c501d2829909244f0798de2939910b3 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 21 Aug 2021 20:07:39 +0400 Subject: [PATCH 13/17] [AFR] added tokens and images download support; --- .../sources/ScryfallImageSupportTokens.java | 24 +++++++++++++++++ .../src/main/resources/card-pictures-tok.txt | 23 +++++++++++++++- .../mage/cards/a/ArlinnVoiceOfThePack.java | 2 +- .../mage/cards/t/TheBookOfVileDarkness.java | 5 +++- .../src/mage/cards/w/WolfSkullShaman.java | 2 +- .../src/mage/cards/w/WolfcallersHowl.java | 2 +- .../src/mage/cards/w/WrensRunPackmaster.java | 2 +- .../mage/game/permanent/token/BooToken.java | 5 ++++ .../mage/game/permanent/token/DevilToken.java | 5 ++++ .../permanent/token/DogIllusionToken.java | 8 +++++- .../game/permanent/token/GoblinToken.java | 2 +- .../game/permanent/token/GuenhwyvarToken.java | 8 +++++- .../token/IcingdeathFrostTongueToken.java | 10 +++++++ .../permanent/token/LolthSpiderToken.java | 12 +++++++-- .../game/permanent/token/SkeletonToken.java | 4 +++ .../game/permanent/token/TheAtropalToken.java | 6 +++++ .../game/permanent/token/TreasureToken.java | 2 +- .../mage/game/permanent/token/VecnaToken.java | 8 +++++- .../mage/game/permanent/token/WolfToken.java | 27 +++++-------------- .../game/permanent/token/ZombieToken.java | 2 +- 20 files changed, 125 insertions(+), 34 deletions(-) diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java index bc5003a66d4..a90b82d63d9 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java @@ -613,6 +613,30 @@ public class ScryfallImageSupportTokens { put("MH2/Zombie Army", "https://api.scryfall.com/cards/tmh2/7/en?format=image"); put("MH2/Zombie", "https://api.scryfall.com/cards/tmh2/6/en?format=image"); + // AFR + put("AFR/Angel", "https://api.scryfall.com/cards/tafr/1/en?format=image"); + put("AFR/Boo", "https://api.scryfall.com/cards/tafr/10/en?format=image"); + put("AFR/Devil", "https://api.scryfall.com/cards/tafr/11/en?format=image"); + put("AFR/Dog Illusion", "https://api.scryfall.com/cards/tafr/3/en?format=image"); + put("AFR/Dungeon of the Mad Mage", "https://api.scryfall.com/cards/tafr/20/en?format=image"); + put("AFR/Emblem Ellywick Tumblestrum", "https://api.scryfall.com/cards/tafr/16/en?format=image"); + put("AFR/Faerie Dragon", "https://api.scryfall.com/cards/tafr/4/en?format=image"); + put("AFR/Goblin", "https://api.scryfall.com/cards/tafr/12/en?format=image"); + put("AFR/Guenhwyvar", "https://api.scryfall.com/cards/tafr/13/en?format=image"); + put("AFR/Icingdeath, Frost Tongue", "https://api.scryfall.com/cards/tafr/2/en?format=image"); + put("AFR/Emblem Lolth, Spider Queen", "https://api.scryfall.com/cards/tafr/17/en?format=image"); + put("AFR/Lost Mine of Phandelver", "https://api.scryfall.com/cards/tafr/21/en?format=image"); + put("AFR/Emblem Mordenkainen", "https://api.scryfall.com/cards/tafr/18/en?format=image"); + put("AFR/Skeleton", "https://api.scryfall.com/cards/tafr/6/en?format=image"); + put("AFR/Spider", "https://api.scryfall.com/cards/tafr/7/en?format=image"); + put("AFR/The Atropal", "https://api.scryfall.com/cards/tafr/5/en?format=image"); + put("AFR/Tomb of Annihilation", "https://api.scryfall.com/cards/tafr/22/en?format=image"); + put("AFR/Treasure", "https://api.scryfall.com/cards/tafr/15/en?format=image"); + put("AFR/Vecna", "https://api.scryfall.com/cards/tafr/8/en?format=image"); + put("AFR/Wolf", "https://api.scryfall.com/cards/tafr/14/en?format=image"); + put("AFR/Emblem Zariel, Archduke of Avernus", "https://api.scryfall.com/cards/tafr/19/en?format=image"); + put("AFR/Zombie", "https://api.scryfall.com/cards/tafr/9/en?format=image"); + // generate supported sets supportedSets.clear(); for (String cardName : this.keySet()) { diff --git a/Mage.Client/src/main/resources/card-pictures-tok.txt b/Mage.Client/src/main/resources/card-pictures-tok.txt index b61765bcc87..75ee26ffb0e 100644 --- a/Mage.Client/src/main/resources/card-pictures-tok.txt +++ b/Mage.Client/src/main/resources/card-pictures-tok.txt @@ -108,6 +108,10 @@ |Generate|EMBLEM:KHM|Tyvar Kell||Emblem Tyvar|TyvarKellEmblem| |Generate|EMBLEM:STX|Lukka, Wayward Bonder||Emblem Lukka|LukkaWaywardBonderEmblem| |Generate|EMBLEM:STX|Rowan, Scholar of Sparks||Emblem Rowan|RowanScholarOfSparksEmblem| +|Generate|EMBLEM:AFR|Ellywick Tumblestrum||Emblem Ellywick|EllywickTumblestrumEmblem| +|Generate|EMBLEM:AFR|Lolth, Spider Queen||Emblem Lolth|LolthSpiderQueenEmblem| +|Generate|EMBLEM:AFR|Mordenkainen||Emblem Mordenkainen|MordenkainenEmblem| +|Generate|EMBLEM:AFR|Zariel, Archduke of Avernus||Emblem Zariel|ZarielArchdukeOfAvernusEmblem| # Planes |Generate|PLANE:PCA|Plane - Academy at Tolaria West|||AcademyAtTolariaWestPlane| @@ -1563,4 +1567,21 @@ |Generate|TOK:MH2|Treasure|1||TreasureToken| |Generate|TOK:MH2|Treasure|2||TreasureToken| |Generate|TOK:MH2|Zombie|||ZombieToken| -|Generate|TOK:MH2|Zombie Army|||ZombieArmyToken| \ No newline at end of file +|Generate|TOK:MH2|Zombie Army|||ZombieArmyToken| + +# AFR +|Generate|TOK:AFR|Angel|||Angel33Token| +|Generate|TOK:AFR|Boo|||BooToken| +|Generate|TOK:AFR|Devil|||DevilToken| +|Generate|TOK:AFR|Dog Illusion|||DogIllusionToken| +# |Generate|TOK:AFR|Faerie Dragon|||xxx| TODO: add after dice pr merge +|Generate|TOK:AFR|Goblin|||GoblinToken| +|Generate|TOK:AFR|Guenhwyvar|||GuenhwyvarToken| +|Generate|TOK:AFR|Icingdeath, Frost Tongue|||IcingdeathFrostTongueToken| +|Generate|TOK:AFR|Skeleton|||SkeletonToken| +|Generate|TOK:AFR|Spider|||LolthSpiderToken| +|Generate|TOK:AFR|The Atropal|||TheAtropalToken| +|Generate|TOK:AFR|Treasure|||TreasureToken| +|Generate|TOK:AFR|Vecna|||VecnaToken| +|Generate|TOK:AFR|Wolf|||WolfToken| +|Generate|TOK:AFR|Zombie|||ZombieToken| \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/a/ArlinnVoiceOfThePack.java b/Mage.Sets/src/mage/cards/a/ArlinnVoiceOfThePack.java index 42d180f46d8..0f671dc4c70 100644 --- a/Mage.Sets/src/mage/cards/a/ArlinnVoiceOfThePack.java +++ b/Mage.Sets/src/mage/cards/a/ArlinnVoiceOfThePack.java @@ -34,7 +34,7 @@ public final class ArlinnVoiceOfThePack extends CardImpl { this.addAbility(new SimpleStaticAbility(new ArlinnVoiceOfThePackReplacementEffect())); // -2: Create a 2/2 green Wolf creature token. - this.addAbility(new LoyaltyAbility(new CreateTokenEffect(new WolfToken("WAR")), -2)); + this.addAbility(new LoyaltyAbility(new CreateTokenEffect(new WolfToken()), -2)); } private ArlinnVoiceOfThePack(final ArlinnVoiceOfThePack card) { diff --git a/Mage.Sets/src/mage/cards/t/TheBookOfVileDarkness.java b/Mage.Sets/src/mage/cards/t/TheBookOfVileDarkness.java index a03132e0df6..9e871d501be 100644 --- a/Mage.Sets/src/mage/cards/t/TheBookOfVileDarkness.java +++ b/Mage.Sets/src/mage/cards/t/TheBookOfVileDarkness.java @@ -187,7 +187,10 @@ class TheBookOfVileDarknessEffect extends OneShotEffect { } for (Ability ability : card.getAbilities(game)) { if (ability instanceof TriggeredAbility) { - token.addAbility(ability.copy()); + Ability copyAbility = ability.copy(); + copyAbility.newId(); + copyAbility.setControllerId(source.getControllerId()); + token.addAbility(copyAbility); } } } diff --git a/Mage.Sets/src/mage/cards/w/WolfSkullShaman.java b/Mage.Sets/src/mage/cards/w/WolfSkullShaman.java index 0cf5f3c998a..114b0643e2d 100644 --- a/Mage.Sets/src/mage/cards/w/WolfSkullShaman.java +++ b/Mage.Sets/src/mage/cards/w/WolfSkullShaman.java @@ -26,7 +26,7 @@ public final class WolfSkullShaman extends CardImpl { this.toughness = new MageInt(2); // Kinship - At the beginning of your upkeep, you may look at the top card of your library. If it shares a creature type with Wolf-Skull Shaman, you may reveal it. If you do, create a 2/2 green Wolf creature token. - this.addAbility(new KinshipAbility(new CreateTokenEffect(new WolfToken("LRW")))); + this.addAbility(new KinshipAbility(new CreateTokenEffect(new WolfToken()))); } private WolfSkullShaman(final WolfSkullShaman card) { diff --git a/Mage.Sets/src/mage/cards/w/WolfcallersHowl.java b/Mage.Sets/src/mage/cards/w/WolfcallersHowl.java index c01825bf1e7..d6485568c55 100644 --- a/Mage.Sets/src/mage/cards/w/WolfcallersHowl.java +++ b/Mage.Sets/src/mage/cards/w/WolfcallersHowl.java @@ -72,7 +72,7 @@ class WolfcallersHowlEffect extends OneShotEffect { } } if (count > 0) { - return new CreateTokenEffect(new WolfToken("C14"), count).apply(game, source); + return new CreateTokenEffect(new WolfToken(), count).apply(game, source); } return true; } diff --git a/Mage.Sets/src/mage/cards/w/WrensRunPackmaster.java b/Mage.Sets/src/mage/cards/w/WrensRunPackmaster.java index 3ed40a2a2f8..f8dd682a04b 100644 --- a/Mage.Sets/src/mage/cards/w/WrensRunPackmaster.java +++ b/Mage.Sets/src/mage/cards/w/WrensRunPackmaster.java @@ -41,7 +41,7 @@ public final class WrensRunPackmaster extends CardImpl { this.addAbility(new ChampionAbility(this, SubType.ELF, false)); // {2}{G}: Create a 2/2 green Wolf creature token. - this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new CreateTokenEffect(new WolfToken("LRW")), new ManaCostsImpl<>("{2}{G}"))); + this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new CreateTokenEffect(new WolfToken()), new ManaCostsImpl<>("{2}{G}"))); // Each Wolf you control has deathtouch. Effect effect = new GainAbilityAllEffect(DeathtouchAbility.getInstance(), Duration.WhileOnBattlefield, filter); diff --git a/Mage/src/main/java/mage/game/permanent/token/BooToken.java b/Mage/src/main/java/mage/game/permanent/token/BooToken.java index 08688539217..3f4db588240 100644 --- a/Mage/src/main/java/mage/game/permanent/token/BooToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/BooToken.java @@ -7,6 +7,8 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; +import java.util.Arrays; + /** * @author TheElk801 */ @@ -20,8 +22,11 @@ public final class BooToken extends TokenImpl { subtype.add(SubType.HAMSTER); power = new MageInt(1); toughness = new MageInt(1); + addAbility(TrampleAbility.getInstance()); addAbility(HasteAbility.getInstance()); + + availableImageSetCodes = Arrays.asList("AFR"); } private BooToken(final BooToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/DevilToken.java b/Mage/src/main/java/mage/game/permanent/token/DevilToken.java index 0df41cbc3f3..78708614649 100644 --- a/Mage/src/main/java/mage/game/permanent/token/DevilToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/DevilToken.java @@ -1,6 +1,7 @@ package mage.game.permanent.token; +import java.util.Arrays; import java.util.Collections; import mage.MageInt; @@ -27,11 +28,15 @@ public final class DevilToken extends TokenImpl { color.setRed(true); power = new MageInt(1); toughness = new MageInt(1); + + // When this creature dies, it deals 1 damage to any target. Effect effect = new DamageTargetEffect(1); effect.setText("it deals 1 damage to any target"); Ability ability = new DiesSourceTriggeredAbility(effect); ability.addTarget(new TargetAnyTarget()); this.addAbility(ability); + + availableImageSetCodes = Arrays.asList("SOI", "WAR", "AFR"); } public DevilToken(final DevilToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/DogIllusionToken.java b/Mage/src/main/java/mage/game/permanent/token/DogIllusionToken.java index 2340fbf2ca6..253761eb5b6 100644 --- a/Mage/src/main/java/mage/game/permanent/token/DogIllusionToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/DogIllusionToken.java @@ -13,6 +13,8 @@ import mage.constants.Zone; import mage.game.Game; import mage.players.Player; +import java.util.Arrays; + /** * * @author weirddan455 @@ -27,10 +29,14 @@ public final class DogIllusionToken extends TokenImpl { subtype.add(SubType.ILLUSION); power = new MageInt(0); toughness = new MageInt(0); - addAbility(new SimpleStaticAbility(Zone.ALL, new SetPowerToughnessSourceEffect( + + // This creature's power and toughness are each equal to twice the number of cards in your hand. + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetPowerToughnessSourceEffect( DogIllusionValue.instance, Duration.EndOfGame) .setText("this creature's power and toughness are each equal to twice the number of cards in your hand") )); + + availableImageSetCodes = Arrays.asList("AFR"); } private DogIllusionToken(final DogIllusionToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/GoblinToken.java b/Mage/src/main/java/mage/game/permanent/token/GoblinToken.java index f89bf0fcb6c..d75f24acff8 100644 --- a/Mage/src/main/java/mage/game/permanent/token/GoblinToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/GoblinToken.java @@ -32,7 +32,7 @@ public final class GoblinToken extends TokenImpl { availableImageSetCodes = Arrays.asList("10E", "ALA", "SOM", "M10", "NPH", "M13", "RTR", "MMA", "M15", "C14", "KTK", "EVG", "DTK", "ORI", "DDG", "DDN", "EVG", "MM2", - "MM3", "EMA", "C16", "DOM", "ANA", "RNA", "WAR", "MH1", "TSR", "MH2"); + "MM3", "EMA", "C16", "DOM", "ANA", "RNA", "WAR", "MH1", "TSR", "MH2", "AFR"); } public GoblinToken(final GoblinToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/GuenhwyvarToken.java b/Mage/src/main/java/mage/game/permanent/token/GuenhwyvarToken.java index 12c0f54b9e8..3b32e1ad841 100644 --- a/Mage/src/main/java/mage/game/permanent/token/GuenhwyvarToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/GuenhwyvarToken.java @@ -6,6 +6,8 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; +import java.util.Arrays; + /** * @author TheElk801 */ @@ -19,7 +21,11 @@ public final class GuenhwyvarToken extends TokenImpl { subtype.add(SubType.CAT); power = new MageInt(4); toughness = new MageInt(1); - addAbility(TrampleAbility.getInstance()); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + availableImageSetCodes = Arrays.asList("AFR"); } private GuenhwyvarToken(final GuenhwyvarToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/IcingdeathFrostTongueToken.java b/Mage/src/main/java/mage/game/permanent/token/IcingdeathFrostTongueToken.java index 48aee3717f0..2c4e76b21a1 100644 --- a/Mage/src/main/java/mage/game/permanent/token/IcingdeathFrostTongueToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/IcingdeathFrostTongueToken.java @@ -14,6 +14,8 @@ import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.permanent.DefendingPlayerControlsPredicate; import mage.target.TargetPermanent; +import java.util.Arrays; + /** * @author TheElk801 */ @@ -35,11 +37,19 @@ public class IcingdeathFrostTongueToken extends TokenImpl { cardType.add(CardType.ARTIFACT); color.setWhite(true); subtype.add(SubType.EQUIPMENT); + + // Equipped creature gets +2/+0 this.addAbility(new SimpleStaticAbility(new BoostEquippedEffect(2, 0))); + + // Whenever equipped creature attacks, tap target creature defending player controls Ability ability = new AttacksAttachedTriggeredAbility(new TapTargetEffect(), false); ability.addTarget(new TargetPermanent(filter)); this.addAbility(ability); + + // Equip {2} this.addAbility(new EquipAbility(2)); + + availableImageSetCodes = Arrays.asList("AFR"); } private IcingdeathFrostTongueToken(final IcingdeathFrostTongueToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/LolthSpiderToken.java b/Mage/src/main/java/mage/game/permanent/token/LolthSpiderToken.java index 9e0ecf2ccab..039cda1c577 100644 --- a/Mage/src/main/java/mage/game/permanent/token/LolthSpiderToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/LolthSpiderToken.java @@ -6,6 +6,8 @@ import mage.abilities.keyword.ReachAbility; import mage.constants.CardType; import mage.constants.SubType; +import java.util.Arrays; + /** * @author TheElk801 */ @@ -18,8 +20,14 @@ public final class LolthSpiderToken extends TokenImpl { subtype.add(SubType.SPIDER); power = new MageInt(2); toughness = new MageInt(1); - addAbility(new MenaceAbility()); - addAbility(ReachAbility.getInstance()); + + // Menace + this.addAbility(new MenaceAbility()); + + // Reach + this.addAbility(ReachAbility.getInstance()); + + availableImageSetCodes = Arrays.asList("AFR"); } public LolthSpiderToken(final LolthSpiderToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/SkeletonToken.java b/Mage/src/main/java/mage/game/permanent/token/SkeletonToken.java index 1b140d65ede..237215f4ab6 100644 --- a/Mage/src/main/java/mage/game/permanent/token/SkeletonToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/SkeletonToken.java @@ -4,6 +4,8 @@ import mage.MageInt; import mage.constants.CardType; import mage.constants.SubType; +import java.util.Arrays; + /** * @author TheElk801 */ @@ -16,6 +18,8 @@ public final class SkeletonToken extends TokenImpl { color.setBlack(true); power = new MageInt(1); toughness = new MageInt(1); + + availableImageSetCodes = Arrays.asList("AFR"); } public SkeletonToken(final SkeletonToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/TheAtropalToken.java b/Mage/src/main/java/mage/game/permanent/token/TheAtropalToken.java index 93bd1d89578..2fa1c3b1807 100644 --- a/Mage/src/main/java/mage/game/permanent/token/TheAtropalToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/TheAtropalToken.java @@ -6,6 +6,8 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; +import java.util.Arrays; + /** * @author TheElk801 */ @@ -20,7 +22,11 @@ public final class TheAtropalToken extends TokenImpl { subtype.add(SubType.HORROR); power = new MageInt(4); toughness = new MageInt(4); + + // Deathtouch addAbility(DeathtouchAbility.getInstance()); + + availableImageSetCodes = Arrays.asList("AFR"); } public TheAtropalToken(final TheAtropalToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/TreasureToken.java b/Mage/src/main/java/mage/game/permanent/token/TreasureToken.java index b63d64ef8e3..952ed64833f 100644 --- a/Mage/src/main/java/mage/game/permanent/token/TreasureToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/TreasureToken.java @@ -27,7 +27,7 @@ public final class TreasureToken extends TokenImpl { ability.addCost(new SacrificeSourceCost()); this.addAbility(ability); - availableImageSetCodes = Arrays.asList("XLN", "RNA", "M20", "C19", "C20", "M21", "CMR", "KHM", "STX", "MH2"); + availableImageSetCodes = Arrays.asList("XLN", "RNA", "M20", "C19", "C20", "M21", "CMR", "KHM", "STX", "MH2", "AFR"); } public TreasureToken(final TreasureToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/VecnaToken.java b/Mage/src/main/java/mage/game/permanent/token/VecnaToken.java index a1d4ebf13db..2a9a336db28 100644 --- a/Mage/src/main/java/mage/game/permanent/token/VecnaToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/VecnaToken.java @@ -6,6 +6,8 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; +import java.util.Arrays; + /** * @author TheElk801 */ @@ -20,7 +22,11 @@ public final class VecnaToken extends TokenImpl { subtype.add(SubType.GOD); power = new MageInt(8); toughness = new MageInt(8); - addAbility(IndestructibleAbility.getInstance()); + + // Indestructible + this.addAbility(IndestructibleAbility.getInstance()); + + availableImageSetCodes = Arrays.asList("AFR"); } private VecnaToken(final VecnaToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/WolfToken.java b/Mage/src/main/java/mage/game/permanent/token/WolfToken.java index 95eb587e00c..3c96b8bb03c 100644 --- a/Mage/src/main/java/mage/game/permanent/token/WolfToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/WolfToken.java @@ -3,48 +3,35 @@ package mage.game.permanent.token; import mage.MageInt; import mage.constants.CardType; import mage.constants.SubType; +import mage.util.RandomUtil; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; /** * @author BetaSteward_at_googlemail.com */ public final class WolfToken extends TokenImpl { - static final private List tokenImageSets = new ArrayList<>(); - - static { - tokenImageSets.addAll(Arrays.asList("BNG", "C14", "CNS", "FNMP", "ISD", "LRW", "M10", "M14", "MM2", "SHM", "SOM", - "ZEN", "SOI", "C15", "M15", "WAR", "M20", "THB")); - } - public WolfToken() { - this(null, 0); - } - - public WolfToken(String setCode) { - this(setCode, 0); - } - - public WolfToken(String setCode, int tokenType) { super("Wolf", "2/2 green Wolf creature token"); - availableImageSetCodes = tokenImageSets; - setOriginalExpansionSetCode(setCode); cardType.add(CardType.CREATURE); color.setGreen(true); subtype.add(SubType.WOLF); power = new MageInt(2); toughness = new MageInt(2); + + availableImageSetCodes = Arrays.asList("BNG", "C14", "C15", "CMA", "CMD", "CNS", "DKA", "EVE", "ISD", + "LRW", "M10", "M14", "MM2", "MOR", "SHM", "SOI", "SOM", "V10", "WWK", "ZEN", "WAR", "M20", + "THB", "AFR"); } @Override public void setExpansionSetCodeForImage(String code) { super.setExpansionSetCodeForImage(code); + if (getOriginalExpansionSetCode() != null && getOriginalExpansionSetCode().equals("ISD")) { - this.setTokenType(2); + this.setTokenType(RandomUtil.nextInt(2) + 1); // 2 images } } diff --git a/Mage/src/main/java/mage/game/permanent/token/ZombieToken.java b/Mage/src/main/java/mage/game/permanent/token/ZombieToken.java index 6cad2f2ea2f..b16714cc806 100644 --- a/Mage/src/main/java/mage/game/permanent/token/ZombieToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/ZombieToken.java @@ -25,7 +25,7 @@ public final class ZombieToken extends TokenImpl { "CNS", "MMA", "BNG", "KTK", "DTK", "ORI", "OGW", "SOI", "EMN", "EMA", "MM3", "AKH", "CMA", "E01", "RNA", "WAR", "MH1", "M20", "C19", "THB", "M21", - "CMR", "C21", "MH2"); + "CMR", "C21", "MH2", "AFR"); } @Override From 2b2a2d085a1bcb3c9eaf015b10a76cf5fc5647f0 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 21 Aug 2021 20:29:33 +0400 Subject: [PATCH 14/17] [AFR] added dungeon support in image/card view, images download, Card Viewer, verify tests; --- .../collection/viewer/MageBook.java | 27 ++++++++++++ .../card/images/DownloadPicturesService.java | 6 ++- .../src/main/resources/card-pictures-tok.txt | 5 +++ .../java/mage/verify/VerifyCardDataTest.java | 42 +++++++++++++++++++ .../main/java/mage/game/command/Dungeon.java | 18 ++++---- ...e.java => DungeonOfTheMadMageDungeon.java} | 10 ++--- ....java => LostMineOfPhandelverDungeon.java} | 10 ++--- ...on.java => TombOfAnnihilationDungeon.java} | 14 +++---- 8 files changed, 107 insertions(+), 25 deletions(-) rename Mage/src/main/java/mage/game/command/dungeons/{DungeonOfTheMadMage.java => DungeonOfTheMadMageDungeon.java} (96%) rename Mage/src/main/java/mage/game/command/dungeons/{LostMineOfPhandelver.java => LostMineOfPhandelverDungeon.java} (91%) rename Mage/src/main/java/mage/game/command/dungeons/{TombOfAnnihilation.java => TombOfAnnihilationDungeon.java} (96%) diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java b/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java index 8cfdba4c962..ec431e46028 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java @@ -318,6 +318,33 @@ public class MageBook extends JComponent { } } + // dungeons + List allDungeons = getTokenCardUrls(); + for (CardDownloadData dungeon : allDungeons) { + if (dungeon.getSet().equals(currentSet)) { + try { + String className = dungeon.getName(); + if (dungeon.getTokenClassName() != null && dungeon.getTokenClassName().length() > 0) { + if (dungeon.getTokenClassName().toLowerCase(Locale.ENGLISH).matches(".*dungeon.*")) { + className = dungeon.getTokenClassName(); + className = "mage.game.command.dungeons." + className; + } + } else { + continue; + } + Class c = Class.forName(className); + Constructor cons = c.getConstructor(); + Object newDungeon = cons.newInstance(); + if (newDungeon instanceof Dungeon) { + ((Dungeon) newDungeon).setExpansionSetCodeForImage(currentSet); + res.add(newDungeon); + } + } catch (ClassNotFoundException | InvocationTargetException | IllegalArgumentException | IllegalAccessException | InstantiationException | SecurityException | NoSuchMethodException ex) { + // Swallow exception + } + } + } + return res; } diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java index 414586c8fa8..8e3e97ae7d0 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java @@ -550,7 +550,6 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements CardDownloadData card = new CardDownloadData(params[3], set, "0", false, type, "", "", true); card.setTokenClassName(tokenClassName); list.add(card); - // logger.debug("Token: " + set + "/" + card.getName() + " type: " + type); } else if (params[1].toLowerCase(Locale.ENGLISH).equals("generate") && params[2].startsWith("EMBLEM:")) { String set = params[2].substring(7); CardDownloadData card = new CardDownloadData("Emblem " + params[3], set, "0", false, type, "", "", true, fileName); @@ -571,6 +570,11 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements CardDownloadData card = new CardDownloadData(params[3], set, "0", false, type, "", "", true, fileName); card.setTokenClassName(tokenClassName); list.add(card); + } else if (params[1].toLowerCase(Locale.ENGLISH).equals("generate") && params[2].startsWith("DUNGEON:")) { + String set = params[2].substring(8); + CardDownloadData card = new CardDownloadData(params[3], set, "0", false, type, "", "", true, fileName); + card.setTokenClassName(tokenClassName); + list.add(card); } else { logger.error("wrong line format in tokens file: " + line); } diff --git a/Mage.Client/src/main/resources/card-pictures-tok.txt b/Mage.Client/src/main/resources/card-pictures-tok.txt index 75ee26ffb0e..4ee3e0b4d6f 100644 --- a/Mage.Client/src/main/resources/card-pictures-tok.txt +++ b/Mage.Client/src/main/resources/card-pictures-tok.txt @@ -181,6 +181,11 @@ |Generate|TOK:AER|Gremlin|||GremlinToken| |Generate|TOK:AER|Ragavan|||RagavanToken| +# Dungeons +|Generate|DUNGEON:AFR|Tomb of Annihilation|||TombOfAnnihilationDungeon| +|Generate|DUNGEON:AFR|Lost Mine of Phandelver|||LostMineOfPhandelverDungeon| +|Generate|DUNGEON:AFR|Dungeon of the Mad Mage|||DungeonOfTheMadMageDungeon| + # AKH |Generate|TOK:AKH|Beast|||BeastToken3| |Generate|TOK:AKH|Cat|||CatToken2| diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index 2825e22dd99..b6570834104 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -19,6 +19,7 @@ import mage.constants.CardType; import mage.constants.Rarity; import mage.constants.SubType; import mage.constants.SuperType; +import mage.game.command.Dungeon; import mage.game.command.Plane; import mage.game.draft.DraftCube; import mage.game.draft.RateCard; @@ -1188,6 +1189,47 @@ public class VerifyCardDataTest { } } + @Test + public void test_checkMissingDungeonsData() { + Collection errorsList = new ArrayList<>(); + + Reflections reflections = new Reflections("mage."); + Set> dungeonClassesList = reflections.getSubTypesOf(Dungeon.class); + + // 1. correct class name + for (Class dungeonClass : dungeonClassesList) { + if (!dungeonClass.getName().endsWith("Dungeon")) { + String className = extractShortClass(dungeonClass); + errorsList.add("Error: dungeon class must ends with Dungeon: " + className + " from " + dungeonClass.getName()); + } + } + + // 2. correct package + for (Class dungeonClass : dungeonClassesList) { + String fullClass = dungeonClass.getName(); + if (!fullClass.startsWith("mage.game.command.dungeons.")) { + String className = extractShortClass(dungeonClass); + errorsList.add("Error: dungeon must be stored in mage.game.command.dungeons package: " + className + " from " + dungeonClass.getName()); + } + } + + // 3. correct constructor + for (Class dungeonClass : dungeonClassesList) { + String className = extractShortClass(dungeonClass); + Dungeon dungeon; + try { + dungeon = (Dungeon) createNewObject(dungeonClass); + } catch (Throwable e) { + errorsList.add("Error: can't create dungeon with default constructor: " + className + " from " + dungeonClass.getName()); + } + } + + printMessages(errorsList); + if (errorsList.size() > 0) { + Assert.fail("Found dungeon errors: " + errorsList.size()); + } + } + private void check(Card card, int cardIndex, boolean skipWarning) { MtgJsonCard ref = MtgJsonService.cardFromSet(card.getExpansionSetCode(), card.getName(), card.getCardNumber()); if (ref != null) { diff --git a/Mage/src/main/java/mage/game/command/Dungeon.java b/Mage/src/main/java/mage/game/command/Dungeon.java index eca4f340a17..96c7fe0a9a3 100644 --- a/Mage/src/main/java/mage/game/command/Dungeon.java +++ b/Mage/src/main/java/mage/game/command/Dungeon.java @@ -21,9 +21,9 @@ import mage.constants.Outcome; import mage.constants.SubType; import mage.constants.SuperType; import mage.game.Game; -import mage.game.command.dungeons.DungeonOfTheMadMage; -import mage.game.command.dungeons.LostMineOfPhandelver; -import mage.game.command.dungeons.TombOfAnnihilation; +import mage.game.command.dungeons.DungeonOfTheMadMageDungeon; +import mage.game.command.dungeons.LostMineOfPhandelverDungeon; +import mage.game.command.dungeons.TombOfAnnihilationDungeon; import mage.game.events.GameEvent; import mage.game.events.ZoneChangeEvent; import mage.players.Player; @@ -56,7 +56,7 @@ public class Dungeon implements CommandObject { private MageObject copyFrom; // copied card INFO (used to call original adjusters) private FrameStyle frameStyle; private final Abilities abilites = new AbilitiesImpl<>(); - private final String expansionSetCodeForImage; + private String expansionSetCodeForImage; private final List dungeonRooms = new ArrayList<>(); private DungeonRoom currentRoom = null; @@ -141,11 +141,11 @@ public class Dungeon implements CommandObject { public static Dungeon createDungeon(String name) { switch (name) { case "Tomb of Annihilation": - return new TombOfAnnihilation(); + return new TombOfAnnihilationDungeon(); case "Lost Mine of Phandelver": - return new LostMineOfPhandelver(); + return new LostMineOfPhandelverDungeon(); case "Dungeon of the Mad Mage": - return new DungeonOfTheMadMage(); + return new DungeonOfTheMadMageDungeon(); default: throw new UnsupportedOperationException("A dungeon should have been chosen"); } @@ -322,6 +322,10 @@ public class Dungeon implements CommandObject { return expansionSetCodeForImage; } + public void setExpansionSetCodeForImage(String expansionSetCodeForImage) { + this.expansionSetCodeForImage = expansionSetCodeForImage; + } + @Override public int getZoneChangeCounter(Game game) { return 1; diff --git a/Mage/src/main/java/mage/game/command/dungeons/DungeonOfTheMadMage.java b/Mage/src/main/java/mage/game/command/dungeons/DungeonOfTheMadMageDungeon.java similarity index 96% rename from Mage/src/main/java/mage/game/command/dungeons/DungeonOfTheMadMage.java rename to Mage/src/main/java/mage/game/command/dungeons/DungeonOfTheMadMageDungeon.java index 55cfb0bc4e1..b38d4b5c059 100644 --- a/Mage/src/main/java/mage/game/command/dungeons/DungeonOfTheMadMage.java +++ b/Mage/src/main/java/mage/game/command/dungeons/DungeonOfTheMadMageDungeon.java @@ -26,9 +26,9 @@ import mage.target.common.TargetCreaturePermanent; /** * @author TheElk801 */ -public class DungeonOfTheMadMage extends Dungeon { +public class DungeonOfTheMadMageDungeon extends Dungeon { - public DungeonOfTheMadMage() { + public DungeonOfTheMadMageDungeon() { super("Dungeon of the Mad Mage", "AFR"); // (1) Yawning Portal — You gain 1 life. (→ 2) DungeonRoom yawningPortal = new DungeonRoom("Yawning Portal", new GainLifeEffect(1)); @@ -86,12 +86,12 @@ public class DungeonOfTheMadMage extends Dungeon { this.addRoom(madWizardsLair); } - private DungeonOfTheMadMage(final DungeonOfTheMadMage dungeon) { + private DungeonOfTheMadMageDungeon(final DungeonOfTheMadMageDungeon dungeon) { super(dungeon); } - public DungeonOfTheMadMage copy() { - return new DungeonOfTheMadMage(this); + public DungeonOfTheMadMageDungeon copy() { + return new DungeonOfTheMadMageDungeon(this); } } diff --git a/Mage/src/main/java/mage/game/command/dungeons/LostMineOfPhandelver.java b/Mage/src/main/java/mage/game/command/dungeons/LostMineOfPhandelverDungeon.java similarity index 91% rename from Mage/src/main/java/mage/game/command/dungeons/LostMineOfPhandelver.java rename to Mage/src/main/java/mage/game/command/dungeons/LostMineOfPhandelverDungeon.java index e3fc369b353..c7e4b18c599 100644 --- a/Mage/src/main/java/mage/game/command/dungeons/LostMineOfPhandelver.java +++ b/Mage/src/main/java/mage/game/command/dungeons/LostMineOfPhandelverDungeon.java @@ -18,9 +18,9 @@ import mage.target.common.TargetCreaturePermanent; /** * @author TheElk801 */ -public class LostMineOfPhandelver extends Dungeon { +public class LostMineOfPhandelverDungeon extends Dungeon { - public LostMineOfPhandelver() { + public LostMineOfPhandelverDungeon() { super("Lost Mine of Phandelver", "AFR"); // (1) Cave Entrance — Scry 1. (→ 2a or 2b) DungeonRoom caveEntrance = new DungeonRoom( @@ -75,12 +75,12 @@ public class LostMineOfPhandelver extends Dungeon { this.addRoom(templeOfDumathoin); } - private LostMineOfPhandelver(final LostMineOfPhandelver dungeon) { + private LostMineOfPhandelverDungeon(final LostMineOfPhandelverDungeon dungeon) { super(dungeon); } @Override - public LostMineOfPhandelver copy() { - return new LostMineOfPhandelver(this); + public LostMineOfPhandelverDungeon copy() { + return new LostMineOfPhandelverDungeon(this); } } diff --git a/Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilation.java b/Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilationDungeon.java similarity index 96% rename from Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilation.java rename to Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilationDungeon.java index bdf49b47664..02a250a6ae0 100644 --- a/Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilation.java +++ b/Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilationDungeon.java @@ -28,7 +28,7 @@ import java.util.*; /** * @author TheElk801 */ -public final class TombOfAnnihilation extends Dungeon { +public final class TombOfAnnihilationDungeon extends Dungeon { static final FilterControlledPermanent filter = new FilterControlledPermanent("an artifact, a creature, or a land"); @@ -41,7 +41,7 @@ public final class TombOfAnnihilation extends Dungeon { )); } - public TombOfAnnihilation() { + public TombOfAnnihilationDungeon() { super("Tomb of Annihilation", "AFR"); // (1) Trapped Entry — Each player loses 1 life. (→ 2a or 2b) DungeonRoom trappedEntry = new DungeonRoom("Trapped Entry", new LoseLifeAllPlayersEffect(1)); @@ -71,12 +71,12 @@ public final class TombOfAnnihilation extends Dungeon { this.addRoom(cradleOfTheDeathGod); } - private TombOfAnnihilation(final TombOfAnnihilation dungeon) { + private TombOfAnnihilationDungeon(final TombOfAnnihilationDungeon dungeon) { super(dungeon); } - public TombOfAnnihilation copy() { - return new TombOfAnnihilation(this); + public TombOfAnnihilationDungeon copy() { + return new TombOfAnnihilationDungeon(this); } } @@ -169,7 +169,7 @@ class OublietteTarget extends TargetControlledPermanent { CardType.CREATURE, CardType.LAND ); - private static final FilterControlledPermanent filter = TombOfAnnihilation.filter.copy(); + private static final FilterControlledPermanent filter = TombOfAnnihilationDungeon.filter.copy(); static { filter.setMessage("an artifact, a creature, and a land"); @@ -245,7 +245,7 @@ class SandfallCellEffect extends OneShotEffect { if (player == null) { continue; } - TargetPermanent target = new TargetPermanent(0, 1, TombOfAnnihilation.filter, true); + TargetPermanent target = new TargetPermanent(0, 1, TombOfAnnihilationDungeon.filter, true); player.choose(Outcome.PreventDamage, target, source.getSourceId(), game); map.put(playerId, game.getPermanent(target.getFirstTarget())); } From 0bd402876dfde120f299ff0083c56e2965891704 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 21 Aug 2021 16:35:09 -0400 Subject: [PATCH 15/17] [AFC] Implemented Nihiloor --- Mage.Sets/src/mage/cards/n/Nihiloor.java | 165 ++++++++++++++++++ .../mage/sets/ForgottenRealmsCommander.java | 1 + .../java/mage/abilities/TriggeredAbility.java | 2 + .../mage/abilities/TriggeredAbilityImpl.java | 9 +- 4 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/n/Nihiloor.java diff --git a/Mage.Sets/src/mage/cards/n/Nihiloor.java b/Mage.Sets/src/mage/cards/n/Nihiloor.java new file mode 100644 index 00000000000..d619a6f07eb --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/Nihiloor.java @@ -0,0 +1,165 @@ +package mage.cards.n; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksAllTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.continuous.GainControlTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicate; +import mage.filter.predicate.permanent.TappedPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class Nihiloor extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(); + + static { + filter.add(TargetController.OPPONENT.getOwnerPredicate()); + } + + public Nihiloor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}{U}{B}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.HORROR); + this.power = new MageInt(3); + this.toughness = new MageInt(5); + + // When Nihiloor enters the battlefield, for each opponent, tap up to one untapped creature you control. When you do, gain control of target creature that player controls with power less than or equal to the tapped creature's power for as long as you control Nihiloor. + this.addAbility(new EntersBattlefieldTriggeredAbility(new NihiloorControlEffect())); + + // Whenever you attack with a creature an opponent owns, you gain 2 life and that player loses 2 life. + Ability ability = new AttacksAllTriggeredAbility( + new GainLifeEffect(1), false, filter, + SetTargetPointer.PERMANENT, false + ).setTriggerPhrase("Whenever you attack with a creature an opponent owns, "); + ability.addEffect(new NihiloorLoseLifeEffect()); + this.addAbility(ability); + } + + private Nihiloor(final Nihiloor card) { + super(card); + } + + @Override + public Nihiloor copy() { + return new Nihiloor(this); + } +} + +class NihiloorControlEffect extends OneShotEffect { + + private static final FilterPermanent filter + = new FilterControlledCreaturePermanent("untapped creatured you control"); + + static { + filter.add(TappedPredicate.UNTAPPED); + } + + private static final class NihiloorPredicate implements Predicate { + private final Permanent permanent; + private final UUID playerId; + + private NihiloorPredicate(Permanent permanent, UUID playerId) { + this.permanent = permanent; + this.playerId = playerId; + } + + @Override + public boolean apply(Permanent input, Game game) { + return input.isControlledBy(playerId) + && input.getPower().getValue() <= permanent.getPower().getValue(); + } + } + + NihiloorControlEffect() { + super(Outcome.Benefit); + staticText = "for each opponent, tap up to one untapped creature you control. When you do, " + + "gain control of target creature that player controls with power less than " + + "or equal to the tapped creature's power for as long as you control {this}"; + } + + private NihiloorControlEffect(final NihiloorControlEffect effect) { + super(effect); + } + + @Override + public NihiloorControlEffect copy() { + return new NihiloorControlEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + for (UUID playerId : game.getOpponents(source.getControllerId())) { + Player opponent = game.getPlayer(playerId); + if (opponent == null) { + continue; + } + TargetPermanent target = new TargetPermanent(0, 1, filter, true); + target.withChooseHint("tapping a creature controlled by " + opponent.getName()); + controller.choose(outcome, target, source.getControllerId(), game); + Permanent permanent = game.getPermanent(target.getFirstTarget()); + if (permanent == null || !permanent.tap(source, game)) { + continue; + } + FilterPermanent filter2 = new FilterPermanent( + "creature controlled by " + opponent.getName() + + " with power " + permanent.getPower().getValue() + " or less" + ); + filter2.add(new NihiloorPredicate(permanent, playerId)); + ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility( + new GainControlTargetEffect(Duration.Custom, true), + false, "gain control of target creature that player controls with " + + "power less than or equal to the tapped creature's power for as long as you control {this}" + ); + ability.addTarget(new TargetPermanent(filter2)); + game.fireReflexiveTriggeredAbility(ability, source); + } + return true; + } + +} + +class NihiloorLoseLifeEffect extends OneShotEffect { + + NihiloorLoseLifeEffect() { + super(Outcome.Benefit); + staticText = "you gain 2 life and that player loses 2 life"; + } + + private NihiloorLoseLifeEffect(final NihiloorLoseLifeEffect effect) { + super(effect); + } + + @Override + public NihiloorLoseLifeEffect copy() { + return new NihiloorLoseLifeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(game.getControllerId(getTargetPointer().getFirst(game, source))); + return player != null && player.loseLife(2, game, source, false) > 0; + } +} diff --git a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java index 26c4051ffa7..fd846ecca5c 100644 --- a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java +++ b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java @@ -168,6 +168,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Nature's Lore", 164, Rarity.COMMON, mage.cards.n.NaturesLore.class)); cards.add(new SetCardInfo("Necromantic Selection", 103, Rarity.RARE, mage.cards.n.NecromanticSelection.class)); cards.add(new SetCardInfo("Necrotic Sliver", 188, Rarity.UNCOMMON, mage.cards.n.NecroticSliver.class)); + cards.add(new SetCardInfo("Nihiloor", 53, Rarity.MYTHIC, mage.cards.n.Nihiloor.class)); cards.add(new SetCardInfo("Nimbus Maze", 252, Rarity.RARE, mage.cards.n.NimbusMaze.class)); cards.add(new SetCardInfo("Obsessive Stitcher", 189, Rarity.UNCOMMON, mage.cards.o.ObsessiveStitcher.class)); cards.add(new SetCardInfo("Ogre Slumlord", 104, Rarity.RARE, mage.cards.o.OgreSlumlord.class)); diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbility.java b/Mage/src/main/java/mage/abilities/TriggeredAbility.java index 9387edf1a88..3f4106fe1f6 100644 --- a/Mage/src/main/java/mage/abilities/TriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/TriggeredAbility.java @@ -57,4 +57,6 @@ public interface TriggeredAbility extends Ability { GameEvent getTriggerEvent(); String getTriggerPhrase(); + + TriggeredAbility setTriggerPhrase(String triggerPhrase); } diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java index b0f929e2443..80cef532bb2 100644 --- a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java @@ -25,6 +25,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge protected boolean leavesTheBattlefieldTrigger; private boolean triggersOnce = false; private GameEvent triggerEvent = null; + private String triggerPhrase = null; public TriggeredAbilityImpl(Zone zone, Effect effect) { this(zone, effect, false); @@ -71,6 +72,12 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge ), game.getTurnNum()); } + @Override + public TriggeredAbilityImpl setTriggerPhrase(String triggerPhrase) { + this.triggerPhrase = triggerPhrase; + return this; + } + @Override public void setTriggerEvent(GameEvent triggerEvent) { this.triggerEvent = triggerEvent; @@ -182,7 +189,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge prefix = ""; } - return prefix + getTriggerPhrase() + sb; + return prefix + triggerPhrase == null ? getTriggerPhrase() : triggerPhrase + sb; } @Override From 5547d049383029bf30222c2b52181633f16e0fff Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 21 Aug 2021 16:51:22 -0400 Subject: [PATCH 16/17] [PCY] reworked Celestial Convergence --- .../mage/cards/c/CelestialConvergence.java | 82 ++++++++----------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/Mage.Sets/src/mage/cards/c/CelestialConvergence.java b/Mage.Sets/src/mage/cards/c/CelestialConvergence.java index 08e50f2d490..af565663d98 100644 --- a/Mage.Sets/src/mage/cards/c/CelestialConvergence.java +++ b/Mage.Sets/src/mage/cards/c/CelestialConvergence.java @@ -1,9 +1,5 @@ - package mage.cards.c; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.EntersBattlefieldAbility; @@ -11,7 +7,6 @@ import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.effects.common.counter.RemoveCounterSourceEffect; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -20,10 +15,12 @@ import mage.constants.TargetController; import mage.constants.Zone; import mage.counters.CounterType; import mage.game.Game; +import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.*; + /** - * * @author LevelX2 */ public final class CelestialConvergence extends CardImpl { @@ -70,49 +67,42 @@ class CelestialConvergenceEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Card sourceObject = game.getCard(source.getSourceId()); + Permanent sourcePermanent = source.getSourcePermanentOrLKI(game); Player controller = game.getPlayer(source.getControllerId()); - if (sourceObject != null - && controller != null - && sourceObject.getCounters(game).getCount(CounterType.OMEN) == 0) { + if (sourcePermanent == null || controller == null + || sourcePermanent.getCounters(game).getCount(CounterType.OMEN) > 0) { + return false; + } - /** - * 801.14. If an effect states that a player wins the game, all of - * that player's opponents within their range of influence lose the - * game instead. # - * - * 801.15. If the effect of a spell or ability states that the game - * is a draw, the game is a draw for that spell or ability's - * controller and all players within their range of influence. They - * leave the game. All remaining players continue to play the game. - * - */ - List highestLifePlayers = new ArrayList<>(); - int highLife = Integer.MIN_VALUE; - for (UUID playerId : game.getState().getPlayersInRange(controller.getId(), game)) { - Player player = game.getPlayer(playerId); - if (player != null) { - if (player.getLife() > highLife) { - highestLifePlayers.clear(); - highestLifePlayers.add(player.getId()); - } else if (player.getLife() == highLife) { - highestLifePlayers.add(player.getId()); - } - } - } - if (highestLifePlayers.isEmpty()) { - return false; - } - if (highestLifePlayers.size() > 1) { - game.setDraw(controller.getId()); - } else { - Player winner = game.getPlayer(highestLifePlayers.iterator().next()); - if (winner != null) { - winner.won(game); - } - } + /** + * 801.14. If an effect states that a player wins the game, all of + * that player's opponents within their range of influence lose the + * game instead. # + * + * 801.15. If the effect of a spell or ability states that the game + * is a draw, the game is a draw for that spell or ability's + * controller and all players within their range of influence. They + * leave the game. All remaining players continue to play the game. + * + */ + Map> playerMap = new HashMap<>(); + game.getState() + .getPlayersInRange(controller.getId(), game) + .stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .forEach(player -> playerMap.computeIfAbsent( + player.getLife(), x -> new HashSet<>() + ).add(player.getId())); + int highLife = playerMap.keySet().stream().mapToInt(x -> x).max().orElse(Integer.MIN_VALUE); + if (playerMap.get(highLife).size() > 1) { + game.setDraw(controller.getId()); return true; } - return false; + Player winner = game.getPlayer(playerMap.get(highLife).iterator().next()); + if (winner != null) { + winner.won(game); + } + return true; } } From b1bbe96536b93c4e07f85086213be592b9896a86 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 21 Aug 2021 17:13:03 -0400 Subject: [PATCH 17/17] fixed an error --- Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java index 80cef532bb2..9efd3139270 100644 --- a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java @@ -189,7 +189,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge prefix = ""; } - return prefix + triggerPhrase == null ? getTriggerPhrase() : triggerPhrase + sb; + return prefix + (triggerPhrase == null ? getTriggerPhrase() : triggerPhrase) + sb; } @Override