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.

This commit is contained in:
Christian 'ketura' McCarty
2025-04-07 13:05:08 -05:00
parent a90842b1bf
commit 941201af9f
8 changed files with 268 additions and 26 deletions

View File

@@ -183,11 +183,18 @@
timewords: Assignment timewords: Assignment
effects: { effects: {
type: event type: event
requires: { requires: [
{
type: CanSpotCultureTokens type: CanSpotCultureTokens
culture: isengard culture: isengard
filter: machine filter: machine
memorize: tokenCount
} }
{
type: CanSpot
filter: uruk-hai,MaxTwilight(memory(tokenCount))
}
]
cost: [ cost: [
{ {
type: ChooseANumber type: ChooseANumber
@@ -199,7 +206,6 @@
} }
memorize: removed memorize: removed
} }
], ],
effect: [ effect: [
{ {

View File

@@ -241,7 +241,7 @@ public class ValueResolver {
return (game, cardAffected) -> { return (game, cardAffected) -> {
int result = 0; int result = 0;
for (PhysicalCard physicalCard : Filters.filterActive(game, filterable)) { for (PhysicalCard physicalCard : Filters.filterActive(game, filterable)) {
result += game.getModifiersQuerying().getTwilightCostToPlay(game, physicalCard, null, 0, false); result += game.getModifiersQuerying().getCurrentTwilightCost(game, physicalCard);
} }
return result; return result;
}; };

View File

@@ -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", parameterFilters.put("inskirmishagainst",
(parameter, environment) -> { (parameter, environment) -> {
final FilterableSource filterableSource = environment.getFilterFactory().generateFilter(parameter, environment); final FilterableSource filterableSource = environment.getFilterFactory().generateFilter(parameter, environment);

View File

@@ -1,10 +1,8 @@
package com.gempukku.lotro.cards.build.field.effect.requirement; package com.gempukku.lotro.cards.build.field.effect.requirement;
import com.gempukku.lotro.cards.build.CardGenerationEnvironment; import com.gempukku.lotro.cards.build.*;
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.field.FieldUtils; 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.Culture;
import com.gempukku.lotro.common.Filterable; import com.gempukku.lotro.common.Filterable;
import com.gempukku.lotro.common.Token; import com.gempukku.lotro.common.Token;
@@ -15,12 +13,13 @@ import org.json.simple.JSONObject;
public class CanSpotCultureTokens implements RequirementProducer { public class CanSpotCultureTokens implements RequirementProducer {
@Override @Override
public Requirement getPlayRequirement(JSONObject object, CardGenerationEnvironment environment) throws InvalidCardDefinitionException { 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 String filter = FieldUtils.getString(object.get("filter"), "filter", "any");
final Culture culture = FieldUtils.getEnum(Culture.class, object.get("culture"), "culture"); final Culture culture = FieldUtils.getEnum(Culture.class, object.get("culture"), "culture");
final Token tokenForCulture = culture != null ? Token.findTokenForCulture(culture) : null; 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); final FilterableSource filterableSource = environment.getFilterFactory().generateFilter(filter, environment);
@@ -28,13 +27,22 @@ public class CanSpotCultureTokens implements RequirementProducer {
return (actionContext) -> { return (actionContext) -> {
LotroGame game = actionContext.getGame(); LotroGame game = actionContext.getGame();
Filterable[] filters = new Filterable[]{filterableSource.getFilterable(actionContext)}; Filterable[] filters = new Filterable[]{filterableSource.getFilterable(actionContext)};
int count = valueSource.getEvaluator(actionContext).evaluateExpression(actionContext.getGame(), null);
return GameUtils.getAllSpottableCultureTokens(game, filters) >= count; return GameUtils.getAllSpottableCultureTokens(game, filters) >= count;
}; };
} else { } else {
return (actionContext) -> { return (actionContext) -> {
LotroGame game = actionContext.getGame(); LotroGame game = actionContext.getGame();
Filterable[] filters = new Filterable[]{filterableSource.getFilterable(actionContext)}; 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;
}; };
} }
} }

View File

@@ -106,7 +106,7 @@ public class GameUtils {
return result; return result;
} }
public static List<PhysicalCard> getRandomCards(List<? extends PhysicalCard> cards, int count) { public static List<PhysicalCard> getRandomCards(Collection<? extends PhysicalCard> cards, int count) {
List<PhysicalCard> randomizedCards = new ArrayList<>(cards); List<PhysicalCard> randomizedCards = new ArrayList<>(cards);
Collections.shuffle(randomizedCards, ThreadLocalRandom.current()); Collections.shuffle(randomizedCards, ThreadLocalRandom.current());

View File

@@ -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 @Override
public int getTwilightCostToPlay(LotroGame game, PhysicalCard physicalCard, PhysicalCard target, int twilightCostModifier, boolean ignoreRoamingPenalty) { public int getTwilightCostToPlay(LotroGame game, PhysicalCard physicalCard, PhysicalCard target, int twilightCostModifier, boolean ignoreRoamingPenalty) {
LoggingThreadLocal.logMethodStart(physicalCard, "getTwilightCost"); LoggingThreadLocal.logMethodStart(physicalCard, "getTwilightCost");

View File

@@ -52,6 +52,7 @@ public interface ModifiersQuerying {
// Twilight cost // Twilight cost
int getTwilightCostToPlay(LotroGame game, PhysicalCard physicalCard, PhysicalCard target, int twilightCostModifier, boolean ignoreRoamingPenalty); int getTwilightCostToPlay(LotroGame game, PhysicalCard physicalCard, PhysicalCard target, int twilightCostModifier, boolean ignoreRoamingPenalty);
int getCurrentTwilightCost(LotroGame game, PhysicalCard physicalCard);
int getRoamingPenalty(LotroGame game, PhysicalCard physicalCard); int getRoamingPenalty(LotroGame game, PhysicalCard physicalCard);

View File

@@ -3,7 +3,6 @@ package com.gempukku.lotro.cards.unofficial.pc.vsets.set_v02;
import com.gempukku.lotro.cards.GenericCardTestHelper; import com.gempukku.lotro.cards.GenericCardTestHelper;
import com.gempukku.lotro.common.*; import com.gempukku.lotro.common.*;
import com.gempukku.lotro.game.CardNotFoundException; import com.gempukku.lotro.game.CardNotFoundException;
import com.gempukku.lotro.game.PhysicalCardImpl;
import com.gempukku.lotro.logic.decisions.DecisionResultInvalidException; import com.gempukku.lotro.logic.decisions.DecisionResultInvalidException;
import org.junit.Test; import org.junit.Test;
@@ -18,8 +17,14 @@ public class Card_V2_021_Tests
return new GenericCardTestHelper( return new GenericCardTestHelper(
new HashMap<>() new HashMap<>()
{{ {{
put("card", "102_21"); put("shot", "102_21");
// put other cards in here as needed for the test case 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.FellowshipSites,
GenericCardTestHelper.FOTRFrodo, GenericCardTestHelper.FOTRFrodo,
@@ -44,7 +49,7 @@ public class Card_V2_021_Tests
var scn = GetScenario(); var scn = GetScenario();
var card = scn.GetFreepsCard("card"); var card = scn.GetFreepsCard("shot");
assertEquals("Breaching Shot", card.getBlueprint().getTitle()); assertEquals("Breaching Shot", card.getBlueprint().getTitle());
assertNull(card.getBlueprint().getSubtitle()); assertNull(card.getBlueprint().getSubtitle());
@@ -56,18 +61,191 @@ public class Card_V2_021_Tests
assertEquals(2, card.getBlueprint().getTwilightCost()); assertEquals(2, card.getBlueprint().getTwilightCost());
} }
// Uncomment any @Test markers below once this is ready to be used @Test
//@Test public void BreachingShotCanRemove3TokensToAssignAnUrukCosting3() throws DecisionResultInvalidException, CardNotFoundException {
public void BreachingShotTest1() throws DecisionResultInvalidException, CardNotFoundException {
//Pre-game setup //Pre-game setup
var scn = GetScenario(); var scn = GetScenario();
var card = scn.GetFreepsCard("card"); var shot = scn.GetShadowCard("shot");
scn.FreepsMoveCardToHand(card); 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.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());
} }
} }