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()); } }