From 941201af9f40311b3a4120e4920fa06512b2386e Mon Sep 17 00:00:00 2001 From: Christian 'ketura' McCarty Date: Mon, 7 Apr 2025 13:05:08 -0500 Subject: [PATCH] Fixed V2 Breaching Shot being playable if there aren't enough tokens on machines to assign an uruk, and also fixed it crashing the game if not enough tokens were removed to assign an uruk. --- .../unofficial/pc/setV02/V2-Isengard.hjson | 18 +- .../appender/resolver/ValueResolver.java | 2 +- .../field/effect/filter/FilterFactory.java | 44 ++++ .../requirement/CanSpotCultureTokens.java | 22 +- .../com/gempukku/lotro/logic/GameUtils.java | 2 +- .../lotro/logic/modifiers/ModifiersLogic.java | 5 + .../logic/modifiers/ModifiersQuerying.java | 1 + .../pc/vsets/set_v02/Card_V2_021_Tests.java | 200 +++++++++++++++++- 8 files changed, 268 insertions(+), 26 deletions(-) diff --git a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/setV02/V2-Isengard.hjson b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/setV02/V2-Isengard.hjson index ff0efa5cb..993a15bb6 100644 --- a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/setV02/V2-Isengard.hjson +++ b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/setV02/V2-Isengard.hjson @@ -183,11 +183,18 @@ timewords: Assignment effects: { type: event - requires: { - type: CanSpotCultureTokens - culture: isengard - filter: machine - } + requires: [ + { + type: CanSpotCultureTokens + culture: isengard + filter: machine + memorize: tokenCount + } + { + type: CanSpot + filter: uruk-hai,MaxTwilight(memory(tokenCount)) + } + ] cost: [ { type: ChooseANumber @@ -199,7 +206,6 @@ } memorize: removed } - ], effect: [ { diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/resolver/ValueResolver.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/resolver/ValueResolver.java index 02ac30835..7d08afcba 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/resolver/ValueResolver.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/resolver/ValueResolver.java @@ -241,7 +241,7 @@ public class ValueResolver { return (game, cardAffected) -> { int result = 0; for (PhysicalCard physicalCard : Filters.filterActive(game, filterable)) { - result += game.getModifiersQuerying().getTwilightCostToPlay(game, physicalCard, null, 0, false); + result += game.getModifiersQuerying().getCurrentTwilightCost(game, physicalCard); } return result; }; diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/filter/FilterFactory.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/filter/FilterFactory.java index 930725692..f17141845 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/filter/FilterFactory.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/filter/FilterFactory.java @@ -406,6 +406,50 @@ public class FilterFactory { ); }; }); + parameterFilters.put("highesttwilight", + (parameter, environment) -> { + final FilterableSource filterableSource = environment.getFilterFactory().generateFilter(parameter, environment); + return actionContext -> { + final Filterable sourceFilterable = filterableSource.getFilterable(actionContext); + return Filters.and( + sourceFilterable, Filters.strengthEqual( + new SingleMemoryEvaluator( + new Evaluator() { + @Override + public int evaluateExpression(LotroGame game, PhysicalCard cardAffected) { + int maxTwilight = Integer.MIN_VALUE; + for (PhysicalCard card : Filters.filterActive(game, sourceFilterable)) + maxTwilight = Math.max(maxTwilight, game.getModifiersQuerying().getCurrentTwilightCost(game, card)); + return maxTwilight; + } + } + ) + ) + ); + }; + }); + parameterFilters.put("lowesttwilight", + (parameter, environment) -> { + final FilterableSource filterableSource = environment.getFilterFactory().generateFilter(parameter, environment); + return actionContext -> { + final Filterable sourceFilterable = filterableSource.getFilterable(actionContext); + return Filters.and( + sourceFilterable, Filters.strengthEqual( + new SingleMemoryEvaluator( + new Evaluator() { + @Override + public int evaluateExpression(LotroGame game, PhysicalCard cardAffected) { + int minTwilight = Integer.MAX_VALUE; + for (PhysicalCard card : Filters.filterActive(game, sourceFilterable)) + minTwilight = Math.min(minTwilight, game.getModifiersQuerying().getCurrentTwilightCost(game, card)); + return minTwilight; + } + } + ) + ) + ); + }; + }); parameterFilters.put("inskirmishagainst", (parameter, environment) -> { final FilterableSource filterableSource = environment.getFilterFactory().generateFilter(parameter, environment); diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/requirement/CanSpotCultureTokens.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/requirement/CanSpotCultureTokens.java index 26d481d1a..7063d1232 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/requirement/CanSpotCultureTokens.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/requirement/CanSpotCultureTokens.java @@ -1,10 +1,8 @@ package com.gempukku.lotro.cards.build.field.effect.requirement; -import com.gempukku.lotro.cards.build.CardGenerationEnvironment; -import com.gempukku.lotro.cards.build.FilterableSource; -import com.gempukku.lotro.cards.build.InvalidCardDefinitionException; -import com.gempukku.lotro.cards.build.Requirement; +import com.gempukku.lotro.cards.build.*; import com.gempukku.lotro.cards.build.field.FieldUtils; +import com.gempukku.lotro.cards.build.field.effect.appender.resolver.ValueResolver; import com.gempukku.lotro.common.Culture; import com.gempukku.lotro.common.Filterable; import com.gempukku.lotro.common.Token; @@ -15,12 +13,13 @@ import org.json.simple.JSONObject; public class CanSpotCultureTokens implements RequirementProducer { @Override public Requirement getPlayRequirement(JSONObject object, CardGenerationEnvironment environment) throws InvalidCardDefinitionException { - FieldUtils.validateAllowedFields(object, "culture", "filter", "amount"); + FieldUtils.validateAllowedFields(object, "culture", "filter", "amount", "memorize"); - final int count = FieldUtils.getInteger(object.get("amount"), "amount", 1); + final ValueSource valueSource = ValueResolver.resolveEvaluator(object.get("amount"), 1, environment); final String filter = FieldUtils.getString(object.get("filter"), "filter", "any"); final Culture culture = FieldUtils.getEnum(Culture.class, object.get("culture"), "culture"); final Token tokenForCulture = culture != null ? Token.findTokenForCulture(culture) : null; + final String memory = FieldUtils.getString(object.get("memorize"), "memorize"); final FilterableSource filterableSource = environment.getFilterFactory().generateFilter(filter, environment); @@ -28,13 +27,22 @@ public class CanSpotCultureTokens implements RequirementProducer { return (actionContext) -> { LotroGame game = actionContext.getGame(); Filterable[] filters = new Filterable[]{filterableSource.getFilterable(actionContext)}; + int count = valueSource.getEvaluator(actionContext).evaluateExpression(actionContext.getGame(), null); + return GameUtils.getAllSpottableCultureTokens(game, filters) >= count; }; } else { return (actionContext) -> { LotroGame game = actionContext.getGame(); Filterable[] filters = new Filterable[]{filterableSource.getFilterable(actionContext)}; - return GameUtils.getSpottableCultureTokensOfType(game, tokenForCulture, filters) >= count; + int count = valueSource.getEvaluator(actionContext).evaluateExpression(actionContext.getGame(), null); + int tokens = GameUtils.getSpottableCultureTokensOfType(game, tokenForCulture, filters); + + if (memory != null) { + actionContext.setValueToMemory(memory, String.valueOf(tokens)); + } + + return tokens >= count; }; } } diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/GameUtils.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/GameUtils.java index 0974b8e0e..79508d007 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/GameUtils.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/GameUtils.java @@ -106,7 +106,7 @@ public class GameUtils { return result; } - public static List getRandomCards(List cards, int count) { + public static List getRandomCards(Collection cards, int count) { List randomizedCards = new ArrayList<>(cards); Collections.shuffle(randomizedCards, ThreadLocalRandom.current()); diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/modifiers/ModifiersLogic.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/modifiers/ModifiersLogic.java index 2f37b6231..f00973fc3 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/modifiers/ModifiersLogic.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/modifiers/ModifiersLogic.java @@ -492,6 +492,11 @@ public class ModifiersLogic implements ModifiersEnvironment, ModifiersQuerying { } } + @Override + public int getCurrentTwilightCost(LotroGame game, PhysicalCard physicalCard) { + return getTwilightCostToPlay(game, physicalCard, null, 0, true); + } + @Override public int getTwilightCostToPlay(LotroGame game, PhysicalCard physicalCard, PhysicalCard target, int twilightCostModifier, boolean ignoreRoamingPenalty) { LoggingThreadLocal.logMethodStart(physicalCard, "getTwilightCost"); diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/modifiers/ModifiersQuerying.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/modifiers/ModifiersQuerying.java index 7379b54b6..055654cb2 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/modifiers/ModifiersQuerying.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/modifiers/ModifiersQuerying.java @@ -52,6 +52,7 @@ public interface ModifiersQuerying { // Twilight cost int getTwilightCostToPlay(LotroGame game, PhysicalCard physicalCard, PhysicalCard target, int twilightCostModifier, boolean ignoreRoamingPenalty); + int getCurrentTwilightCost(LotroGame game, PhysicalCard physicalCard); int getRoamingPenalty(LotroGame game, PhysicalCard physicalCard); diff --git a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/unofficial/pc/vsets/set_v02/Card_V2_021_Tests.java b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/unofficial/pc/vsets/set_v02/Card_V2_021_Tests.java index 72e463e93..9bfca79c6 100644 --- a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/unofficial/pc/vsets/set_v02/Card_V2_021_Tests.java +++ b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/unofficial/pc/vsets/set_v02/Card_V2_021_Tests.java @@ -3,7 +3,6 @@ package com.gempukku.lotro.cards.unofficial.pc.vsets.set_v02; import com.gempukku.lotro.cards.GenericCardTestHelper; import com.gempukku.lotro.common.*; import com.gempukku.lotro.game.CardNotFoundException; -import com.gempukku.lotro.game.PhysicalCardImpl; import com.gempukku.lotro.logic.decisions.DecisionResultInvalidException; import org.junit.Test; @@ -18,8 +17,14 @@ public class Card_V2_021_Tests return new GenericCardTestHelper( new HashMap<>() {{ - put("card", "102_21"); - // put other cards in here as needed for the test case + put("shot", "102_21"); + put("ram", "5_44"); + put("ladder", "5_57"); + put("fighter", "1_146"); // 3 + put("guard", "1_147"); // 4 + + put("merry", "1_302"); + }}, GenericCardTestHelper.FellowshipSites, GenericCardTestHelper.FOTRFrodo, @@ -44,7 +49,7 @@ public class Card_V2_021_Tests var scn = GetScenario(); - var card = scn.GetFreepsCard("card"); + var card = scn.GetFreepsCard("shot"); assertEquals("Breaching Shot", card.getBlueprint().getTitle()); assertNull(card.getBlueprint().getSubtitle()); @@ -56,18 +61,191 @@ public class Card_V2_021_Tests assertEquals(2, card.getBlueprint().getTwilightCost()); } - // Uncomment any @Test markers below once this is ready to be used - //@Test - public void BreachingShotTest1() throws DecisionResultInvalidException, CardNotFoundException { + @Test + public void BreachingShotCanRemove3TokensToAssignAnUrukCosting3() throws DecisionResultInvalidException, CardNotFoundException { //Pre-game setup var scn = GetScenario(); - var card = scn.GetFreepsCard("card"); - scn.FreepsMoveCardToHand(card); + var shot = scn.GetShadowCard("shot"); + var ladder = scn.GetShadowCard("ladder"); + var fighter = scn.GetShadowCard("fighter"); + var guard = scn.GetShadowCard("guard"); + scn.ShadowMoveCardToHand(shot); + scn.ShadowMoveCardToSupportArea(ladder); + scn.ShadowMoveCharToTable(fighter, guard); + + var frodo = scn.GetRingBearer(); + var merry = scn.GetFreepsCard("merry"); + scn.FreepsMoveCharToTable(merry); scn.StartGame(); - scn.FreepsPlayCard(card); + scn.AddTokensToCard(ladder, 3); + scn.SkipToPhase(Phase.ASSIGNMENT); + scn.FreepsPassCurrentPhaseAction(); - assertEquals(2, scn.GetTwilight()); + assertTrue(scn.ShadowPlayAvailable(shot)); + assertEquals(3, scn.GetCultureTokensOn(ladder)); + assertEquals(3, fighter.getBlueprint().getTwilightCost()); + assertEquals(4, guard.getBlueprint().getTwilightCost()); + assertFalse(scn.IsCharAssigned(merry)); + assertFalse(scn.IsCharAssigned(fighter)); + assertFalse(scn.IsCharAssigned(guard)); + scn.ShadowPlayCard(shot); + scn.ShadowChoose("3"); + + assertEquals(0, scn.GetCultureTokensOn(ladder)); + assertTrue(scn.IsCharAssigned(merry)); + assertTrue(scn.IsCharAssigned(fighter)); + assertFalse(scn.IsCharAssigned(guard)); + } + + @Test + public void BreachingShotCanRemove3TokensFromVariousMachinesToAssignAnUrukCosting3() throws DecisionResultInvalidException, CardNotFoundException { + //Pre-game setup + var scn = GetScenario(); + + var shot = scn.GetShadowCard("shot"); + var ladder = scn.GetShadowCard("ladder"); + var ram = scn.GetShadowCard("ram"); + var fighter = scn.GetShadowCard("fighter"); + var guard = scn.GetShadowCard("guard"); + scn.ShadowMoveCardToHand(shot); + scn.ShadowMoveCardToSupportArea(ladder, ram); + scn.ShadowMoveCharToTable(fighter, guard); + + var merry = scn.GetFreepsCard("merry"); + scn.FreepsMoveCharToTable(merry); + + scn.StartGame(); + scn.AddTokensToCard(ladder, 2); + scn.AddTokensToCard(ram, 2); + scn.SkipToPhase(Phase.ASSIGNMENT); + scn.FreepsPassCurrentPhaseAction(); + + assertTrue(scn.ShadowPlayAvailable(shot)); + assertEquals(2, scn.GetCultureTokensOn(ladder)); + assertEquals(2, scn.GetCultureTokensOn(ram)); + assertEquals(3, fighter.getBlueprint().getTwilightCost()); + assertEquals(4, guard.getBlueprint().getTwilightCost()); + assertFalse(scn.IsCharAssigned(merry)); + assertFalse(scn.IsCharAssigned(fighter)); + assertFalse(scn.IsCharAssigned(guard)); + scn.ShadowPlayCard(shot); + scn.ShadowChoose("3"); + + assertEquals(2, scn.ShadowGetCardChoiceCount()); //2 machines with tokens + scn.ShadowChooseCard(ram); + scn.ShadowChooseCard(ladder); + scn.ShadowChooseCard(ram); + + assertEquals(0, scn.GetCultureTokensOn(ram)); + assertEquals(1, scn.GetCultureTokensOn(ladder)); + assertTrue(scn.IsCharAssigned(merry)); + assertTrue(scn.IsCharAssigned(fighter)); + assertFalse(scn.IsCharAssigned(guard)); + } + + @Test + public void BreachingShotCanRemove4TokensToAssignAnUrukCosting3Or4() throws DecisionResultInvalidException, CardNotFoundException { + //Pre-game setup + var scn = GetScenario(); + + var shot = scn.GetShadowCard("shot"); + var ladder = scn.GetShadowCard("ladder"); + var fighter = scn.GetShadowCard("fighter"); + var guard = scn.GetShadowCard("guard"); + scn.ShadowMoveCardToHand(shot); + scn.ShadowMoveCardToSupportArea(ladder); + scn.ShadowMoveCharToTable(fighter, guard); + + var frodo = scn.GetRingBearer(); + var merry = scn.GetFreepsCard("merry"); + scn.FreepsMoveCharToTable(merry); + + scn.StartGame(); + scn.AddTokensToCard(ladder, 4); + scn.SkipToPhase(Phase.ASSIGNMENT); + scn.FreepsPassCurrentPhaseAction(); + + assertTrue(scn.ShadowPlayAvailable(shot)); + assertEquals(4, scn.GetCultureTokensOn(ladder)); + assertEquals(3, fighter.getBlueprint().getTwilightCost()); + assertEquals(4, guard.getBlueprint().getTwilightCost()); + assertFalse(scn.IsCharAssigned(merry)); + assertFalse(scn.IsCharAssigned(fighter)); + assertFalse(scn.IsCharAssigned(guard)); + scn.ShadowPlayCard(shot); + scn.ShadowChoose("4"); + + assertEquals(2, scn.ShadowGetCardChoiceCount()); //Both uruks, one costing 3, the other 4 + scn.ShadowChooseCard(guard); + + assertEquals(0, scn.GetCultureTokensOn(ladder)); + assertTrue(scn.IsCharAssigned(merry)); + assertFalse(scn.IsCharAssigned(fighter)); + assertTrue(scn.IsCharAssigned(guard)); + } + + @Test + public void BreachingShotNotPlayableIfFewerTokensThanTwilightCostOfUruk() throws DecisionResultInvalidException, CardNotFoundException { + //Pre-game setup + var scn = GetScenario(); + + var shot = scn.GetShadowCard("shot"); + var ladder = scn.GetShadowCard("ladder"); + var fighter = scn.GetShadowCard("fighter"); + var guard = scn.GetShadowCard("guard"); + scn.ShadowMoveCardToHand(shot); + scn.ShadowMoveCardToSupportArea(ladder); + scn.ShadowMoveCharToTable(fighter, guard); + + var frodo = scn.GetRingBearer(); + var merry = scn.GetFreepsCard("merry"); + scn.FreepsMoveCharToTable(merry); + + scn.StartGame(); + scn.AddTokensToCard(ladder, 2); + scn.SkipToPhase(Phase.ASSIGNMENT); + scn.FreepsPassCurrentPhaseAction(); + + assertFalse(scn.ShadowPlayAvailable(shot)); + } + + @Test + public void BreachingShotDoesNotCrashIfTooFewTokensAreRemovedToAssign() throws DecisionResultInvalidException, CardNotFoundException { + //Pre-game setup + var scn = GetScenario(); + + var shot = scn.GetShadowCard("shot"); + var ladder = scn.GetShadowCard("ladder"); + var fighter = scn.GetShadowCard("fighter"); + var guard = scn.GetShadowCard("guard"); + scn.ShadowMoveCardToHand(shot); + scn.ShadowMoveCardToSupportArea(ladder); + scn.ShadowMoveCharToTable(fighter, guard); + + var frodo = scn.GetRingBearer(); + var merry = scn.GetFreepsCard("merry"); + scn.FreepsMoveCharToTable(merry); + + scn.StartGame(); + scn.AddTokensToCard(ladder, 3); + scn.SkipToPhase(Phase.ASSIGNMENT); + scn.FreepsPassCurrentPhaseAction(); + + assertTrue(scn.ShadowPlayAvailable(shot)); + assertEquals(3, scn.GetCultureTokensOn(ladder)); + + assertFalse(scn.IsCharAssigned(merry)); + assertFalse(scn.IsCharAssigned(fighter)); + assertFalse(scn.IsCharAssigned(guard)); + scn.ShadowPlayCard(shot); + scn.ShadowChoose("2"); + + assertEquals(1, scn.GetCultureTokensOn(ladder)); + assertFalse(scn.IsCharAssigned(merry)); + assertFalse(scn.IsCharAssigned(fighter)); + assertFalse(scn.IsCharAssigned(guard)); + assertTrue(scn.FreepsAnyDecisionsAvailable()); } }