V2 Breached Wall implementation

- Altering all cards that "spot a site you control" and similar to use the count-alteration-aware ControlsSite instead of spotting (except in the case where it's checking for the existence of a literal site, such as in preparation for stacking on a controlled site)
- Altering all cards providing a bonus "for each site you control" to use forEachSiteYouControl instead of forEachYouCanSpot
- Altered existing ControlsSite and OpponentDoesNotControlSite to incorporate site control count modifiers
This commit is contained in:
Christian 'ketura' McCarty
2024-11-04 11:06:10 -06:00
parent 1e04a80cf7
commit 8cae3615f2
28 changed files with 168 additions and 152 deletions

View File

@@ -954,8 +954,7 @@
type: activated
phase: shadow
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
}
cost: {
type: exert
@@ -1059,8 +1058,7 @@
modifier: {
type: skipPhase
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 2
}
phase: archery
@@ -1071,8 +1069,7 @@
modifier: {
type: addKeyword
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 3
}
filter: your,culture(dunland),man
@@ -1084,8 +1081,7 @@
modifier: {
type: addKeyword
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 3
}
filter: your,culture(dunland),man
@@ -1575,8 +1571,7 @@
type: activated
phase: assignment
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
}
cost: {
type: removeTwilight
@@ -1814,8 +1809,7 @@
effects: {
type: event
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 2
}
effect: [

View File

@@ -1479,8 +1479,7 @@
type: exert
select: choose(unbound,companion)
count: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
}
}
]
@@ -1768,8 +1767,7 @@
type: activated
phase: skirmish
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
}
cost: {
type: removeTwilight
@@ -3654,7 +3652,8 @@
type: activated
phase: regroup
requires: {
type: controlsSite
type: canSpot
filter: controlledSite
}
effect: {
type: StackCardsFromPlay
@@ -3784,7 +3783,8 @@
type: activated
phase: regroup
requires: {
type: controlsSite
type: canSpot
filter: controlledSite
}
effect: {
type: StackCardsFromPlay
@@ -3846,8 +3846,7 @@
type: modifyStrength
filter: self
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
multiplier: 2
}
}
@@ -3893,8 +3892,7 @@
{
type: modifyOwnCost
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
multiplier: -1
}
}
@@ -3939,8 +3937,7 @@
{
type: modifyOwnCost
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
multiplier: -1
}
}
@@ -3985,8 +3982,7 @@
{
type: modifyOwnCost
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
multiplier: -1
}
}

View File

@@ -1784,8 +1784,7 @@
}
side: shadow
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
}
}
}
@@ -2228,8 +2227,7 @@
effect: {
type: addTwilight
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
multiplier: 2
}
}

View File

@@ -664,8 +664,7 @@
effect: {
type: addTwilight
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
}
}
}

View File

@@ -552,8 +552,7 @@
effect: {
type: drawCards
count: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
limit: 3
}
}

View File

@@ -113,8 +113,7 @@
type: modifyStrength
filter: self
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 2
}
amount: 5
@@ -126,8 +125,7 @@
type: addKeyword
filter: self
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 2
}
keyword: fierce
@@ -139,8 +137,7 @@
type: addKeyword
filter: self
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 2
}
keyword: damage
@@ -404,8 +401,7 @@
type: activated
phase: shadow
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 2
}
cost: {

View File

@@ -167,8 +167,7 @@
player: fp
select: choose(companion)
count: {
type: forEachYouCanSpot
filter: SiteYouControl
type: ForEachSiteYouControl
}
}
}

View File

@@ -34,8 +34,7 @@
effect: {
type: addTwilight
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
}
}
}

View File

@@ -389,8 +389,7 @@
type: modifyCost
filter: your,culture(sauron)
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
divider: 2
multiplier: -1
}
@@ -530,8 +529,7 @@
effect: {
type: addBurdens
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
}
}
}
@@ -584,8 +582,7 @@
type: modifyStrength
select: choose(culture(sauron),minion)
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
multiplier: {
type: Conditional
requires: {
@@ -1102,8 +1099,16 @@
{
type: toPlay
requires: {
type: canSpot
filter: or(besieger,controlledSite)
type: Or
requires: [
{
type: ControlsSite
}
{
type: canSpot
filter: besieger
}
]
}
}
{
@@ -3115,8 +3120,7 @@
type: modifyStrength
filter: besieger
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
}
}
}

View File

@@ -166,8 +166,16 @@
type: activated
phase: assignment
requires: {
type: canSpot
filter: or(and(culture(sauron),engine),controlledSite)
type: Or
requires: [
{
type: ControlsSite
}
{
type: canSpot
filter: culture(sauron),engine
}
]
}
effect: {
type: assignFpCharacterToSkirmish

View File

@@ -119,8 +119,7 @@
type: modifyStrength
filter: memory(chosenMinion)
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
}
}
until: start(regroup)

View File

@@ -114,8 +114,7 @@
{
type: modifyOwnCost
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
multiplier: -1
}
}

View File

@@ -299,8 +299,7 @@
type: modifyStrength
select: choose(culture(sauron),minion)
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
multiplier: 2
}
memorize: chosenMinion
@@ -733,8 +732,7 @@
player: free people
select: choose(companion)
count: {
type: forEachYouCanSpot
filter: SiteYouControl
type: ForEachSiteYouControl
}
}
}
@@ -798,13 +796,11 @@
filter: bearer,name(Gothmog)
}
true: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
multiplier: 2
}
false: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
}
}
}

View File

@@ -48,8 +48,7 @@
times: 2
}
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
}
effect: {
type: addTwilight
@@ -161,8 +160,7 @@
text: Make a {men} Man strength +2
phase: skirmish
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
}
cost: {
type: removeTwilight
@@ -179,8 +177,7 @@
text: Make a {men} Man strength +3
phase: skirmish
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 2
}
cost: {
@@ -198,8 +195,7 @@
text: Make a {men} Man strength +4
phase: skirmish
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 3
}
cost: {

View File

@@ -32,8 +32,7 @@
type: modifyStrength
filter: self
amount: {
type: forEachYouCanSpot
filter: controlledSite
type: ForEachSiteYouControl
}
}
}
@@ -1037,8 +1036,7 @@
type: cantTakeWounds
requires: [
{
type: canSpot
filter: controlledSite
type: ControlsSite
count: 2
}
{

View File

@@ -246,8 +246,7 @@
modifier: {
type: addKeyword
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 2
}
filter: self
@@ -259,8 +258,7 @@
modifier: {
type: modifyStrength
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 4
}
filter: culture(men),minion
@@ -272,8 +270,7 @@
modifier: {
type: addKeyword
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 4
}
filter: culture(men),minion
@@ -762,8 +759,7 @@
effect: {
type: repeat
times: {
type: forEachYouCanSpot
filter: controlledSite
type: forEachSiteYouControl
}
effect: {
type: exert
@@ -824,8 +820,7 @@
filter: self
}
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
}
effect: {
type: exhaust

View File

@@ -29,8 +29,7 @@
modifier: {
type: addKeyword
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 2
}
filter: bearer
@@ -43,8 +42,7 @@
modifier: {
type: addKeyword
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
count: 4
}
filter: bearer
@@ -158,8 +156,7 @@
amount: {
type: Conditional
requires: {
type: canSpot
filter: controlledSite
type: ControlsSite
}
true: 3
false: 2
@@ -722,8 +719,7 @@
filter: self
keyword: hunter
amount: {
type: forEachYouCanSpot
filter: siteYouControl
type: forEachSiteYouControl
multiplier: 3
}
}
@@ -941,8 +937,7 @@
effect: {
type: repeat
times: {
type: forEachYouCanSpot
filter: siteYouControl
type: ForEachSiteYouControl
}
effect: {
type: exert

View File

@@ -474,7 +474,14 @@ public class ValueResolver {
return new SmartValueSource(environment, object,
actionContext -> {
String performingPlayer = actionContext.getPerformingPlayer();
return (game, cardAffected) -> GameUtils.getSpottableControlledSitesCount(game, performingPlayer);
return (game, cardAffected) -> GameUtils.getControlledSitesCountByPlayer(game, performingPlayer);
});
} else if (type.equalsIgnoreCase("forEachSiteOpponentControls")) {
FieldUtils.validateAllowedFields(object, "over", "limit", "multiplier", "divider");
return new SmartValueSource(environment, object,
actionContext -> {
String performingPlayer = actionContext.getPerformingPlayer();
return (game, cardAffected) -> GameUtils.getControlledSitesCountOfOpponents(game, performingPlayer);
});
} else if (type.equalsIgnoreCase("forEachStacked")) {
FieldUtils.validateAllowedFields(object, "on", "filter", "over", "limit", "multiplier", "divider");

View File

@@ -53,9 +53,9 @@ public class FilterFactory {
simpleFilters.put("controlledbyshadowplayer",
(actionContext -> Filters.siteControlledByOtherPlayer(actionContext.getGame().getGameState().getCurrentPlayerId())));
simpleFilters.put("controlledsite",
(actionContext -> Filters.siteControlled(actionContext.getPerformingPlayer())));
(actionContext -> Filters.siteControlledByPlayer(actionContext.getPerformingPlayer())));
simpleFilters.put("siteyoucontrol",
(actionContext -> Filters.siteControlled(actionContext.getPerformingPlayer())));
(actionContext -> Filters.siteControlledByPlayer(actionContext.getPerformingPlayer())));
simpleFilters.put("cultureindeadpile",
new FilterableSource() {
@Override

View File

@@ -1,18 +0,0 @@
package com.gempukku.lotro.cards.build.field.effect.requirement;
import com.gempukku.lotro.cards.build.CardGenerationEnvironment;
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.logic.GameUtils;
import org.json.simple.JSONObject;
public class CanSpotControlledSites implements RequirementProducer {
@Override
public Requirement getPlayRequirement(JSONObject object, CardGenerationEnvironment environment) throws InvalidCardDefinitionException {
FieldUtils.validateAllowedFields(object, "amount");
final int amount = FieldUtils.getInteger(object.get("amount"), "amount");
return (actionContext) -> GameUtils.getSpottableControlledSitesCount(actionContext.getGame(), actionContext.getPerformingPlayer()) >= amount;
}
}

View File

@@ -1,27 +1,28 @@
package com.gempukku.lotro.cards.build.field.effect.requirement;
import com.gempukku.lotro.cards.build.CardGenerationEnvironment;
import com.gempukku.lotro.cards.build.InvalidCardDefinitionException;
import com.gempukku.lotro.cards.build.PlayerSource;
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.PlayerResolver;
import com.gempukku.lotro.cards.build.field.effect.appender.resolver.ValueResolver;
import com.gempukku.lotro.filters.Filters;
import com.gempukku.lotro.game.state.LotroGame;
import com.gempukku.lotro.logic.GameUtils;
import org.json.simple.JSONObject;
public class ControlsSite implements RequirementProducer {
@Override
public Requirement getPlayRequirement(JSONObject object, CardGenerationEnvironment environment) throws InvalidCardDefinitionException {
FieldUtils.validateAllowedFields(object, "player");
FieldUtils.validateAllowedFields(object, "player", "count");
final String player = FieldUtils.getString(object.get("player"), "player", "you");
final ValueSource countSource = ValueResolver.resolveEvaluator(object.get("count"), 1, environment);
final PlayerSource playerSource = PlayerResolver.resolvePlayer(player);
return (actionContext) -> {
LotroGame game = actionContext.getGame();
String playerId = playerSource.getPlayer(actionContext);
return Filters.findFirstActive(game, Filters.siteControlled(playerId)) != null;
final int count = countSource.getEvaluator(actionContext).evaluateExpression(actionContext.getGame(), null);
return GameUtils.getControlledSitesCountByPlayer(actionContext.getGame(),
playerSource.getPlayer(actionContext)) >= count;
};
}
}

View File

@@ -7,6 +7,7 @@ import com.gempukku.lotro.cards.build.Requirement;
import com.gempukku.lotro.cards.build.field.FieldUtils;
import com.gempukku.lotro.cards.build.field.effect.appender.resolver.PlayerResolver;
import com.gempukku.lotro.filters.Filters;
import com.gempukku.lotro.logic.GameUtils;
import org.json.simple.JSONObject;
public class OpponentDoesNotControlSite implements RequirementProducer {
@@ -17,7 +18,6 @@ public class OpponentDoesNotControlSite implements RequirementProducer {
final String player = FieldUtils.getString(object.get("player"), "player", "you");
final PlayerSource playerSource = PlayerResolver.resolvePlayer(player);
return (actionContext) ->
Filters.countActive(actionContext.getGame(), Filters.siteControlledByOtherPlayer(playerSource.getPlayer(actionContext))) == 0;
return (actionContext) -> GameUtils.getControlledSitesCountOfOpponents(actionContext.getGame(), playerSource.getPlayer(actionContext)) <= 0;
}
}

View File

@@ -27,7 +27,6 @@ public class RequirementFactory {
requirementProducers.put("cantspot", new CantSpot());
requirementProducers.put("canspotburdens", new CanSpotBurdens());
requirementProducers.put("cantspotburdens", new CantSpotBurdens());
requirementProducers.put("canspotcontrolledsites", new CanSpotControlledSites());
requirementProducers.put("canspotculturetokens", new CanSpotCultureTokens());
requirementProducers.put("canspotfpcultures", new CanSpotFPCultures());
requirementProducers.put("cantspotfpcultures", new CantSpotFPCultures());

View File

@@ -575,7 +575,7 @@ public class Filters {
return (game, physicalCard) -> physicalCard.getBlueprint().getCardType() == CardType.SITE && physicalCard.getCardController() != null && !physicalCard.getCardController().equals(thisPlayer);
}
public static Filter siteControlled(final String playerId) {
public static Filter siteControlledByPlayer(final String playerId) {
return (game, physicalCard) -> physicalCard.getBlueprint().getCardType() == CardType.SITE && playerId.equals(physicalCard.getCardController());
}

View File

@@ -324,7 +324,16 @@ public class GameUtils {
return game.getModifiersQuerying().getNumberOfSpottableShadowCultures(game, playerId);
}
public static int getSpottableControlledSitesCount(LotroGame game, String playerId) {
public static int getControlledSitesCountByPlayer(LotroGame game, String playerId) {
return game.getModifiersQuerying().getNumberOfSpottableControlledSites(game, playerId);
}
public static int getControlledSitesCountOfOpponents(LotroGame game, String playerId) {
int total = 0;
for(var opponent : getOpponents(game, playerId)) {
total += getControlledSitesCountByPlayer(game, opponent);
}
return total;
}
}

View File

@@ -1026,7 +1026,7 @@ public class ModifiersLogic implements ModifiersEnvironment, ModifiersQuerying {
@Override
public int getNumberOfSpottableControlledSites(LotroGame game, String playerId) {
int result = Filters.filterActive(game, Filters.siteControlled(playerId)).size();
int result = Filters.filterActive(game, Filters.siteControlledByPlayer(playerId)).size();
for (Modifier modifier : getModifiers(game, ModifierEffect.SPOT_MODIFIER)) {
result += modifier.getSiteControlledSpotCountModifier(game, playerId);

View File

@@ -39,7 +39,7 @@ public class WinConditionRule {
} else if (game.getFormat().winOnControlling5Sites()
&& effectResults.getType() == EffectResult.Type.CONTROL_SITE) {
for (String opponent : GameUtils.getShadowPlayers(game)) {
if (Filters.countActive(game, CardType.SITE, Filters.siteControlled(opponent)) >= 5)
if (Filters.countActive(game, CardType.SITE, Filters.siteControlledByPlayer(opponent)) >= 5)
game.playerWon(opponent, "Controls 5 sites");
}
}

View File

@@ -18,10 +18,21 @@ public class Card_V2_066_Tests
return new GenericCardTestHelper(
new HashMap<>()
{{
put("card", "102_66");
// put other cards in here as needed for the test case
put("haldir", "4_71");
put("hillman", "15_82");
put("mob", "4_205");
}},
new HashMap<>() {{
put("site1", "1_319");
put("site2", "1_327");
put("site3", "1_337");
put("site4", "1_343");
put("site5", "102_66");
put("site6", "1_351");
put("site7", "1_353");
put("site8", "1_356");
put("site9", "1_360");
}},
GenericCardTestHelper.FellowshipSites,
GenericCardTestHelper.FOTRFrodo,
GenericCardTestHelper.RulingRing
);
@@ -40,14 +51,12 @@ public class Card_V2_066_Tests
* Type: Site
* Subtype:
* Site Number: 5T
* Game Text: Battleground. While at this site, the count of sites the shadow player controls is +1.
* Game Text: Battleground. The count of sites the shadow player controls is +1.
*/
var scn = GetScenario();
//Use this once you have set the deck up properly
//var card = scn.GetFreepsSite(5);
var card = scn.GetFreepsCard("card");
var card = scn.GetFreepsSite(5);
assertEquals("Breached Wall", card.getBlueprint().getTitle());
assertNull(card.getBlueprint().getSubtitle());
@@ -58,18 +67,57 @@ public class Card_V2_066_Tests
assertEquals(5, card.getBlueprint().getSiteNumber());
}
// Uncomment any @Test markers below once this is ready to be used
//@Test
public void BreachedWallTest1() throws DecisionResultInvalidException, CardNotFoundException {
@Test
public void BreachedWallBonusAffectsAllSiteControlMechanics() throws DecisionResultInvalidException, CardNotFoundException {
//Pre-game setup
var scn = GetScenario();
var card = scn.GetFreepsCard("card");
scn.FreepsMoveCardToHand(card);
var wall = scn.GetShadowSite(5);
var haldir = scn.GetFreepsCard("haldir");
scn.FreepsMoveCharToTable(haldir);
var hillman = scn.GetShadowCard("hillman");
var mob = scn.GetShadowCard("mob");
scn.ShadowMoveCardToHand(hillman, mob);
scn.StartGame();
scn.FreepsPlayCard(card);
assertEquals(6, scn.GetTwilight());
scn.SkipToSite(3);
scn.SetTwilight(12);
scn.FreepsPassCurrentPhaseAction(); // we're now on 4, not quite Breached Wall
//No opponent controls a site, so Haldir is strength +2
assertEquals(7, scn.getStrength(haldir));
assertEquals(15, scn.GetTwilight());
scn.ShadowMoveCharToTable(hillman);
//Is base 9 strength, not boosted or fierce from controlling a site
assertEquals(9, scn.getStrength(hillman));
assertFalse(scn.hasKeyword(hillman, Keyword.FIERCE));
scn.ShadowPlayCard(mob);
//15 - full undiscounted 5 -2 for roaming = 8
assertEquals(8, scn.GetTwilight());
scn.ShadowMoveCardToHand(hillman, mob);
scn.SkipToPhase(Phase.REGROUP);
scn.PassCurrentPhaseActions();
scn.ShadowDeclineReconciliation();
scn.FreepsChooseToMove();
assertEquals(wall, scn.GetCurrentSite());
//Opponent site control count is +1, so Haldir loses bonus
assertEquals(5, scn.getStrength(haldir));
assertEquals(16, scn.GetTwilight());
scn.ShadowMoveCharToTable(hillman);
//Is base 9 strength +3 and fierce from controlling a site
assertEquals(12, scn.getStrength(hillman));
assertTrue(scn.hasKeyword(hillman, Keyword.FIERCE));
scn.ShadowPlayCard(mob);
//16 - 5 full cost + 1 discount for controlling 1 site = 12
assertEquals(12, scn.GetTwilight());
}
}