From 1a5b4ff47ef4325f9c557b9d17637a2a5504e1a4 Mon Sep 17 00:00:00 2001 From: Christian 'ketura' McCarty Date: Sun, 24 Nov 2024 00:21:03 -0600 Subject: [PATCH] Fixing Power and Terror not being playable if the twilight pool does not contain at least enough to pay for an undiscounted Balrog (even if the discount would make it playable) --- .../cards/official/set02/set02-Moria.hjson | 9 ++++ .../effect/appender/PlayCardFromHand.java | 12 ++++- .../appender/resolver/CardResolver.java | 18 ++++--- .../official/set02/Card_02_070_Tests.java | 47 +++++++++++++------ 4 files changed, 64 insertions(+), 22 deletions(-) diff --git a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/official/set02/set02-Moria.hjson b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/official/set02/set02-Moria.hjson index 1a0f6db2f..347ad74e1 100644 --- a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/official/set02/set02-Moria.hjson +++ b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/official/set02/set02-Moria.hjson @@ -1046,6 +1046,15 @@ memory: revealedCards multiplier: -2 } + //As a play-from-hand effect, this has to be checked for playability before + // the event resolves, even tho it's an effect and not a cost. Thus we have to + // provide the maximum theoretical discount so we can play this card even if + // there is less twilight in the pool than the Balrog costs. + maxDiscount: { + type: ForEachInHand + filter: culture(moria),orc + multiplier: -2 + } } } gametext: Shadow: Reveal any number of [moria] Orcs from your hand to play The Balrog. Its twilight cost is -2 for each Orc revealed. diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/PlayCardFromHand.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/PlayCardFromHand.java index cf15169c9..c290202c5 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/PlayCardFromHand.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/PlayCardFromHand.java @@ -24,11 +24,12 @@ import java.util.Collection; public class PlayCardFromHand implements EffectAppenderProducer { @Override public EffectAppender createEffectAppender(JSONObject effectObject, CardGenerationEnvironment environment) throws InvalidCardDefinitionException { - FieldUtils.validateAllowedFields(effectObject, "select", "on", "discount", "removedTwilight", "ignoreInDeadPile", "ignoreRoamingPenalty", "memorize"); + FieldUtils.validateAllowedFields(effectObject, "select", "on", "discount", "maxDiscount", "removedTwilight", "ignoreInDeadPile", "ignoreRoamingPenalty", "memorize"); final String select = FieldUtils.getString(effectObject.get("select"), "select"); final String onFilter = FieldUtils.getString(effectObject.get("on"), "on"); final ValueSource costModifierSource = ValueResolver.resolveEvaluator(effectObject.get("discount"), 0, environment); + final ValueSource maxDiscountSource = effectObject.get("maxDiscount") == null ? costModifierSource : ValueResolver.resolveEvaluator(effectObject.get("maxDiscount"), 0, environment); final int removedTwilight = FieldUtils.getInteger(effectObject.get("removedTwilight"), "removedTwilight", 0); final boolean ignoreInDeadPile = FieldUtils.getBoolean(effectObject.get("ignoreInDeadPile"), "ignoreInDeadPile", false); final boolean ignoreRoamingPenalty = FieldUtils.getBoolean(effectObject.get("ignoreRoamingPenalty"), "ignoreRoamingPenalty", false); @@ -50,6 +51,15 @@ public class PlayCardFromHand implements EffectAppenderProducer { } return Filters.playable(game, removedTwilight, costModifier, ignoreRoamingPenalty, ignoreInDeadPile, true); }, + (actionContext) -> { + final LotroGame game = actionContext.getGame(); + final int maxDiscountModifier = maxDiscountSource.getEvaluator(actionContext).evaluateExpression(game, actionContext.getSource()); + if (onFilterableSource != null) { + final Filterable onFilterable = onFilterableSource.getFilterable(actionContext); + return Filters.and(Filters.playable(game, maxDiscountModifier, ignoreRoamingPenalty, ignoreInDeadPile), ExtraFilters.attachableTo(game, maxDiscountModifier, onFilterable)); + } + return Filters.playable(game, removedTwilight, maxDiscountModifier, ignoreRoamingPenalty, ignoreInDeadPile, true); + }, actionContext -> new ConstantEvaluator(1), memorize, "you", "you", "Choose card to play from hand", false, environment)); result.addEffectAppender( new DelayedAppender() { diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/resolver/CardResolver.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/resolver/CardResolver.java index 197c11706..cc2642523 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/resolver/CardResolver.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/resolver/CardResolver.java @@ -111,14 +111,18 @@ public class CardResolver { } public static EffectAppender resolveCardsInHand(String type, ValueSource countSource, String memory, String choicePlayer, String handPlayer, String choiceText, CardGenerationEnvironment environment) throws InvalidCardDefinitionException { - return resolveCardsInHand(type, null, countSource, memory, choicePlayer, handPlayer, choiceText, false, environment); + return resolveCardsInHand(type, null, null, countSource, memory, choicePlayer, handPlayer, choiceText, false, environment); } public static EffectAppender resolveCardsInHand(String type, ValueSource countSource, String memory, String choicePlayer, String handPlayer, String choiceText, boolean showMatchingOnly, CardGenerationEnvironment environment) throws InvalidCardDefinitionException { - return resolveCardsInHand(type, null, countSource, memory, choicePlayer, handPlayer, choiceText, showMatchingOnly, environment); + return resolveCardsInHand(type, null, null, countSource, memory, choicePlayer, handPlayer, choiceText, showMatchingOnly, environment); } - public static EffectAppender resolveCardsInHand(String type, FilterableSource additionalFilter, ValueSource countSource, String memory, String choicePlayer, String handPlayer, String choiceText, boolean showMatchingOnly, CardGenerationEnvironment environment) throws InvalidCardDefinitionException { + public static EffectAppender resolveCardsInHand(String type, FilterableSource choiceFilter, ValueSource countSource, String memory, String choicePlayer, String handPlayer, String choiceText, boolean showMatchingOnly, CardGenerationEnvironment environment) throws InvalidCardDefinitionException { + return resolveCardsInHand(type, choiceFilter, choiceFilter, countSource, memory, choicePlayer, handPlayer, choiceText, showMatchingOnly, environment); + } + + public static EffectAppender resolveCardsInHand(String type, FilterableSource choiceFilter, FilterableSource playabilityFilter, ValueSource countSource, String memory, String choicePlayer, String handPlayer, String choiceText, boolean showMatchingOnly, CardGenerationEnvironment environment) throws InvalidCardDefinitionException { final PlayerSource handSource = PlayerResolver.resolvePlayer(handPlayer); Function> cardSource = actionContext -> { String handPlayer1 = handSource.getPlayer(actionContext); @@ -148,11 +152,11 @@ public class CardResolver { } }; } else if (type.equals("self")) { - return resolveSelf(additionalFilter, additionalFilter, countSource, memory, cardSource); + return resolveSelf(choiceFilter, playabilityFilter, countSource, memory, cardSource); } else if (type.startsWith("memory(") && type.endsWith(")")) { - return resolveMemoryCards(type, additionalFilter, additionalFilter, countSource, memory, cardSource); + return resolveMemoryCards(type, choiceFilter, playabilityFilter, countSource, memory, cardSource); } else if (type.startsWith("all(") && type.endsWith(")")) { - return resolveAllCards(type, additionalFilter, memory, environment, cardSource); + return resolveAllCards(type, choiceFilter, memory, environment, cardSource); } else if (type.startsWith("choose(") && type.endsWith(")")) { final PlayerSource playerSource = PlayerResolver.resolvePlayer(choicePlayer); ChoiceEffectSource effectSource = (possibleCards, action, actionContext, min, max) -> { @@ -181,7 +185,7 @@ public class CardResolver { } }; - return resolveChoiceCards(type, additionalFilter, additionalFilter, countSource, environment, cardSource, effectSource); + return resolveChoiceCards(type, choiceFilter, playabilityFilter, countSource, environment, cardSource, effectSource); } throw new InvalidCardDefinitionException("Unable to resolve card resolver of type: " + type); } diff --git a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/official/set02/Card_02_070_Tests.java b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/official/set02/Card_02_070_Tests.java index 48fdd44e1..c9acdb287 100644 --- a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/official/set02/Card_02_070_Tests.java +++ b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/official/set02/Card_02_070_Tests.java @@ -1,10 +1,7 @@ package com.gempukku.lotro.cards.official.set02; import com.gempukku.lotro.cards.GenericCardTestHelper; -import com.gempukku.lotro.common.CardType; -import com.gempukku.lotro.common.Culture; -import com.gempukku.lotro.common.Side; -import com.gempukku.lotro.common.Timeword; +import com.gempukku.lotro.common.*; import com.gempukku.lotro.game.CardNotFoundException; import com.gempukku.lotro.logic.decisions.DecisionResultInvalidException; import org.junit.Test; @@ -20,8 +17,12 @@ public class Card_02_070_Tests return new GenericCardTestHelper( new HashMap<>() {{ - put("card", "2_70"); - // put other cards in here as needed for the test case + put("power", "2_70"); + put("balrog", "6_76"); + + put("orc1", "1_178"); + put("orc2", "1_179"); + put("orc3", "1_181"); }}, GenericCardTestHelper.FellowshipSites, GenericCardTestHelper.FOTRFrodo, @@ -46,7 +47,7 @@ public class Card_02_070_Tests var scn = GetScenario(); - var card = scn.GetFreepsCard("card"); + var card = scn.GetFreepsCard("power"); assertEquals("Power and Terror", card.getBlueprint().getTitle()); assertNull(card.getBlueprint().getSubtitle()); @@ -58,18 +59,36 @@ public class Card_02_070_Tests assertEquals(0, card.getBlueprint().getTwilightCost()); } - // Uncomment any @Test markers below once this is ready to be used - //@Test - public void PowerandTerrorTest1() throws DecisionResultInvalidException, CardNotFoundException { + @Test + public void PowerandTerrorPlaysTheBalrogEvenWhenOnlyPlayableViaDisount() throws DecisionResultInvalidException, CardNotFoundException { //Pre-game setup var scn = GetScenario(); - var card = scn.GetFreepsCard("card"); - scn.FreepsMoveCardToHand(card); + var power = scn.GetShadowCard("power"); + var balrog = scn.GetShadowCard("balrog"); + var orc1 = scn.GetShadowCard("orc1"); + var orc2 = scn.GetShadowCard("orc2"); + var orc3 = scn.GetShadowCard("orc3"); + scn.ShadowMoveCardToHand(power, balrog, orc1, orc2, orc3); scn.StartGame(); - scn.FreepsPlayCard(card); + scn.SetTwilight(7); - assertEquals(0, scn.GetTwilight()); + scn.FreepsPassCurrentPhaseAction(); + + // Starting 7 + 1 companion + 2 from site = 10 + assertEquals(10, scn.GetTwilight()); + assertEquals(14, balrog.getBlueprint().getTwilightCost()); + + // With 3 moria orcs in hand, the maximum discount that Power and Terror can give is -6. + // 14 - 6 + 2 (roaming) = 10, which is exactly what's on the table. P&T should thus permit the play. + assertTrue(scn.ShadowPlayAvailable(power)); + + assertEquals(Zone.HAND, balrog.getZone()); + scn.ShadowPlayCard(power); + assertEquals(3, scn.GetShadowCardChoiceCount()); + scn.ShadowChooseCards(orc1, orc2, orc3); + scn.FreepsDismissRevealedCards(); + assertEquals(Zone.SHADOW_CHARACTERS, balrog.getZone()); } }