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
effects: {
type: event
requires: {
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: [
{

View File

@@ -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;
};

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",
(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;
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;
};
}
}

View File

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

View File

@@ -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);

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