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)

This commit is contained in:
Christian 'ketura' McCarty
2024-11-24 00:21:03 -06:00
parent 5446b06a17
commit 1a5b4ff47e
4 changed files with 64 additions and 22 deletions

View File

@@ -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: <b>Shadow:</b> Reveal any number of [moria] Orcs from your hand to play The Balrog. Its twilight cost is -2 for each Orc revealed.

View File

@@ -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() {

View File

@@ -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<ActionContext, Iterable<? extends PhysicalCard>> 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);
}

View File

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