From 5a5acbe9e5c80eb7c1f305041ccf02f012eeeb15 Mon Sep 17 00:00:00 2001 From: Christian 'ketura' McCarty Date: Sun, 4 Jun 2023 22:23:47 -0500 Subject: [PATCH] Corsair War Galley errata + tests --- .../pc/errata/set08/set08-Raider-errata.hjson | 102 ++++++++++++ .../requirement/CanSpotCultureTokens.java | 31 ++++ .../requirement/RequirementFactory.java | 1 + .../com/gempukku/lotro/filters/Filters.java | 4 + .../com/gempukku/lotro/logic/GameUtils.java | 28 +++- .../lotro/logic/timing/PlayConditions.java | 17 ++ .../lotro/cards/GenericCardTestHelper.java | 15 ++ .../errata/set08/Card_08_059_ErrataTests.java | 146 +++++++++++++----- 8 files changed, 308 insertions(+), 36 deletions(-) create mode 100644 gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/requirement/CanSpotCultureTokens.java diff --git a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/set08/set08-Raider-errata.hjson b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/set08/set08-Raider-errata.hjson index 6a4a77439..1f046c20d 100644 --- a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/set08/set08-Raider-errata.hjson +++ b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/set08/set08-Raider-errata.hjson @@ -283,5 +283,107 @@ } } } + + 78_59: { + cardInfo: { + //Either a full URL, or a subpath for the PC image server + imagePath: errata/LOTR-EN08E059.2_card.jpg + //If this is true, then all gameplay-related info outside this cardInfo definition + // will be ignored and the java class loaded instead. + javaClass: false + //This instructs the blueprint generator to insert this card as an alt of the listed + // parent blueprint. Can of course be ommitted if not an errata or promo. + parent: 8_59 + //This is the tree path to use within the alts structure on the parent. + // Can of course be ommitted if parent is null. + parentPath: errata/pc + //Versioning differentiates releases within a particular alt path, such as PC errata + // version 1 vs version 2. PC version 2 will not conflict with, say, Decipher version 2. + //Top-level cards should always be version 0. + version: 2 + collInfo: 8U59 + rarity: U + setNum: "8" + cardNum: 59 + // Standard, Masterwork, Tengwar, FullArt, etc. Top-level cards are always Standard. + style: Standard + } + title: Corsair War Galley + unique: true + side: Shadow + culture: Raider + twilight: 1 + type: Possession + keyword: Support Area + effects: [ + { + type: trigger + optional: true + trigger: { + type: played + filter: self + } + effect: { + type: addTokens + culture: raider + filter: self + } + } + { + type: modifier + modifier: { + type: shadowhasinitiative + requires: [ + { + type: CanSpotCultureTokens + culture: raider + amount: 6 + } + { + type: canSpot + filter: culture(raider),man + } + ] + } + } + { + type: activated + phase: regroup + cost: { + type: memorizeNumber + memory: numberOfTokens + amount: { + type: forEachToken + culture: raider + filter: any + } + } + effect: [ + { + type: addTwilight + amount: { + type: fromMemory + memory: numberOfTokens + } + } + { + type: discard + filter: self + } + ] + } + ] + gametext: When you play this possession, you may add a [raider] token here.
While you can spot 6 [raider] tokens and a [raider] Man, the Shadow has initiative, regardless of the Free Peoples player's hand.
Regroup: Add (1) for each [raider] token you can spot. Discard this possession. + lore: "" + promotext: "" + alts: { + //These are just CardInfo objects + promos: { + } + //These are full card definitions, with redundant info that is the same as the original card ommitted + errata: { + } + } + } } 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 new file mode 100644 index 000000000..5d8abb9cb --- /dev/null +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/requirement/CanSpotCultureTokens.java @@ -0,0 +1,31 @@ +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.field.FieldUtils; +import com.gempukku.lotro.common.Culture; +import com.gempukku.lotro.common.Token; +import com.gempukku.lotro.logic.timing.PlayConditions; +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"); + + final int count = FieldUtils.getInteger(object.get("amount"), "amount", 1); + final String filter = FieldUtils.getString(object.get("filter"), "filter", "any"); + final Culture culture = FieldUtils.getEnum(Culture.class, object.get("culture"), "culture"); + final Token tokenForCulture = Token.findTokenForCulture(culture); + + if(tokenForCulture == null) + throw new InvalidCardDefinitionException("Culture is required for CanSpotCultureTokens."); + + final FilterableSource filterableSource = environment.getFilterFactory().generateFilter(filter, environment); + + return (actionContext) -> PlayConditions.canSpotCultureTokensOnCards(actionContext.getGame(), tokenForCulture, count, + filterableSource.getFilterable(actionContext)); + } +} diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/requirement/RequirementFactory.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/requirement/RequirementFactory.java index b05f417b6..df7f4daff 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/requirement/RequirementFactory.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/requirement/RequirementFactory.java @@ -18,6 +18,7 @@ public class RequirementFactory { requirementProducers.put("canmove", new CanMove()); requirementProducers.put("canspot", new CanSpot()); requirementProducers.put("canspotburdens", new CanSpotBurdens()); + requirementProducers.put("canspotculturetokens", new CanSpotCultureTokens()); requirementProducers.put("canspotindeadpile", new CanSpotInDeadPile()); requirementProducers.put("canspotthreats", new CanSpotThreats()); requirementProducers.put("canspottwilight", new CanSpotTwilight()); diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/filters/Filters.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/filters/Filters.java index 25682ef53..8f6071a5f 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/filters/Filters.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/filters/Filters.java @@ -222,6 +222,10 @@ public class Filters { }; } + public static Filter hasAnyCultureTokens() { + return hasAnyCultureTokens(1); + } + public static Filter hasAnyCultureTokens(final int count) { return new Filter() { @Override 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 72f7bade2..1457abc76 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 @@ -211,15 +211,41 @@ public class GameUtils { return result; } + // "If you can spot X [elven] tokens..." public static int getSpottableTokensTotal(LotroGame game, Token token) { + return getSpottableCultureTokensOfType(game, token, Filters.any); + } + + // "If you can spot X [elven] tokens on conditions..." + public static int getSpottableCultureTokensOfType(LotroGame game, Token token, Filterable... filters) { int tokensTotal = 0; - for (PhysicalCard physicalCard : Filters.filterActive(game, Filters.hasToken(token))) + final var cards = Filters.filterActive(game, Filters.and(filters, Filters.hasToken(token))); + + for (PhysicalCard physicalCard : cards) tokensTotal += game.getGameState().getTokenCount(physicalCard, token); return tokensTotal; } + // "If you can spot X culture tokens on conditions..." + public static int getAllSpottableCultureTokens(LotroGame game, Filterable... filters) { + int tokensTotal = 0; + + final var cards = Filters.filterActive(game, Filters.and(filters, Filters.hasAnyCultureTokens())); + + for (PhysicalCard physicalCard : cards) { + var tokens = game.getGameState().getTokens(physicalCard); + for(var token : tokens.entrySet()) { + if(token.getKey().getCulture() != null) { + tokensTotal += token.getValue(); + } + } + } + + return tokensTotal; + } + public static int getSpottableCulturesCount(LotroGame game, Filterable... filters) { Set cultures = new HashSet<>(); for (PhysicalCard physicalCard : Filters.filterActive(game, filters)) { diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/timing/PlayConditions.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/timing/PlayConditions.java index 524b33e0b..7dcb67e94 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/timing/PlayConditions.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/logic/timing/PlayConditions.java @@ -254,6 +254,23 @@ public class PlayConditions { return game.getGameState().getBurdens() >= count; } + // "If you can spot X [elven] tokens..." + public static boolean canSpotCultureTokensInPlay(LotroGame game, Token token, int count) { + return GameUtils.getSpottableTokensTotal(game, token) >= count; + } + + // "If you can spot X [elven] tokens on conditions..." + public static boolean canSpotCultureTokensOnCards(LotroGame game, Token token, int count, Filterable... filters) { + return GameUtils.getSpottableCultureTokensOfType(game, token, filters) >= count; + } + + // "If you can spot X culture tokens on conditions..." + // Strictly speaking this would only be needed if there was a card that could add culture tokens to cards + // that did not match their own native culture. + public static boolean canSpotAllCultureTokensOnCards(LotroGame game, int count, Filterable... filters) { + return GameUtils.getAllSpottableCultureTokens(game, filters) >= count; + } + public static boolean canSpotFPCultures(LotroGame game, int count, String playerId) { return GameUtils.getSpottableFPCulturesCount(game, playerId) >= count; } diff --git a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/GenericCardTestHelper.java b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/GenericCardTestHelper.java index 328ee2096..0133b74ec 100644 --- a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/GenericCardTestHelper.java +++ b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/GenericCardTestHelper.java @@ -14,6 +14,7 @@ import com.gempukku.lotro.logic.decisions.DecisionResultInvalidException; import com.gempukku.lotro.logic.effects.DiscardCardsFromPlayEffect; import com.gempukku.lotro.logic.modifiers.Modifier; import com.gempukku.lotro.logic.timing.EffectResult; +import com.gempukku.lotro.logic.timing.PlayConditions; import com.gempukku.lotro.logic.timing.RuleUtils; import com.gempukku.lotro.logic.vo.LotroDeck; import org.junit.Assert; @@ -307,6 +308,10 @@ public class GenericCardTestHelper extends AbstractAtTest { public int GetBurdens() { return _game.getGameState().getBurdens(); } + public boolean FreepsHasInitiative() { return PlayConditions.hasInitiative(_game, Side.FREE_PEOPLE); } + + public boolean ShadowHasInitiative() { return PlayConditions.hasInitiative(_game, Side.SHADOW); } + public int GetFreepsArcheryTotal() { return RuleUtils.calculateFellowshipArcheryTotal(_game); } public int GetShadowArcheryTotal() { return RuleUtils.calculateShadowArcheryTotal(_game); } @@ -455,6 +460,12 @@ public class GenericCardTestHelper extends AbstractAtTest { _game.getGameState().playerDrawsCard(P1); } + public void FreepsDrawCards(int count) { + for(int i = 0; i < count; i++) { + FreepsDrawCard(); + } + } + public void ShadowDrawCard() { _game.getGameState().playerDrawsCard(P2); } @@ -569,6 +580,10 @@ public class GenericCardTestHelper extends AbstractAtTest { _game.getGameState().addTokens(card, Token.findTokenForCulture(card.getBlueprint().getCulture()), count); } + public void RemoveTokensFromCard(PhysicalCardImpl card, int count) { + _game.getGameState().removeTokens(card, Token.findTokenForCulture(card.getBlueprint().getCulture()), count); + } + public void AddThreats(int count) { _game.getGameState().addThreats(_game.getGameState().getCurrentPlayerId(), count); diff --git a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/unofficial/pc/errata/set08/Card_08_059_ErrataTests.java b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/unofficial/pc/errata/set08/Card_08_059_ErrataTests.java index 84cad6c1c..68c12f112 100644 --- a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/unofficial/pc/errata/set08/Card_08_059_ErrataTests.java +++ b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/unofficial/pc/errata/set08/Card_08_059_ErrataTests.java @@ -3,15 +3,12 @@ package com.gempukku.lotro.cards.unofficial.pc.errata.set08; 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 com.gempukku.lotro.logic.modifiers.MoveLimitModifier; import org.junit.Test; import java.util.HashMap; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class Card_08_059_ErrataTests @@ -19,10 +16,14 @@ public class Card_08_059_ErrataTests protected GenericCardTestHelper GetScenario() throws CardNotFoundException, DecisionResultInvalidException { return new GenericCardTestHelper( - new HashMap() + new HashMap<>() {{ - put("card", "58_59"); - // put other cards in here as needed for the test case + put("galley", "78_59"); + put("raider", "4_249"); + + put("chaff1", "4_250"); + put("chaff2", "4_251"); + put("chaff3", "4_252"); }}, GenericCardTestHelper.FellowshipSites, GenericCardTestHelper.FOTRFrodo, @@ -30,55 +31,130 @@ public class Card_08_059_ErrataTests ); } - // Uncomment both @Test markers below once this is ready to be used - - //@Test + @Test public void CorsairWarGalleyStatsAndKeywordsAreCorrect() throws DecisionResultInvalidException, CardNotFoundException { /** * Set: 8 * Title: Corsair War Galley - * Side: Free Peoples + * Unique: True + * Side: SHADOW * Culture: Raider * Twilight Cost: 1 * Type: possession * Subtype: Support Area - * Game Text: When you play this possession, you may add a [Raider] token here. - * While you can spot 4 [Raider] tokens here and a [Raider] Man, the Shadow has initiative, regardless of the Free Peoples player's hand. - * Regroup: Add (1) for each [Raider] token here. Discard this possession. + * Game Text: When you play this possession, you may add a [raider] token here. + * While you can spot 6 [raider] tokens and a [raider] Man, the Shadow has initiative, regardless of the Free Peoples player’s hand.
Regroup: Add (1) for each [raider] token you can spot. Discard this possession. */ //Pre-game setup - GenericCardTestHelper scn = GetScenario(); + var scn = GetScenario(); - PhysicalCardImpl card = scn.GetFreepsCard("card"); - - assertFalse(card.getBlueprint().isUnique()); - assertEquals(Side.SHADOW, card.getBlueprint().getSide()); - assertEquals(Culture.RAIDER, card.getBlueprint().getCulture()); - assertEquals(CardType.POSSESSION, card.getBlueprint().getCardType()); - //assertEquals(Race.CREATURE, card.getBlueprint().getRace()); - assertTrue(scn.HasKeyword(card, Keyword.SUPPORT_AREA)); - assertEquals(1, card.getBlueprint().getTwilightCost()); - //assertEquals(, card.getBlueprint().getStrength()); - //assertEquals(, card.getBlueprint().getVitality()); - //assertEquals(, card.getBlueprint().getResistance()); - //assertEquals(Signet., card.getBlueprint().getSignet()); - //assertEquals(, card.getBlueprint().getSiteNumber()); // Change this to getAllyHomeSiteNumbers for allies + var galley = scn.GetFreepsCard("galley"); + assertTrue(galley.getBlueprint().isUnique()); + assertEquals(Side.SHADOW, galley.getBlueprint().getSide()); + assertEquals(Culture.RAIDER, galley.getBlueprint().getCulture()); + assertEquals(CardType.POSSESSION, galley.getBlueprint().getCardType()); + assertTrue(scn.HasKeyword(galley, Keyword.SUPPORT_AREA)); + assertEquals(1, galley.getBlueprint().getTwilightCost()); } - //@Test - public void CorsairWarGalleyTest1() throws DecisionResultInvalidException, CardNotFoundException { + @Test + public void GalleyAddsTokenOnPlay() throws DecisionResultInvalidException, CardNotFoundException { //Pre-game setup - GenericCardTestHelper scn = GetScenario(); + var scn = GetScenario(); - PhysicalCardImpl card = scn.GetFreepsCard("card"); - scn.FreepsMoveCardToHand(card); + var galley = scn.GetShadowCard("galley"); + scn.ShadowMoveCardToHand(galley); scn.StartGame(); - scn.FreepsPlayCard(card); - assertEquals(1, scn.GetTwilight()); + scn.FreepsPassCurrentPhaseAction(); + + assertTrue(scn.ShadowPlayAvailable(galley)); + scn.ShadowPlayCard(galley); + + assertEquals(0, scn.GetCultureTokensOn(galley)); + assertTrue(scn.ShadowHasOptionalTriggerAvailable()); + scn.ShadowAcceptOptionalTrigger(); + assertEquals(1, scn.GetCultureTokensOn(galley)); + } + + @Test + public void ShadowHasInitiativeIf6TokensAndARaider() throws DecisionResultInvalidException, CardNotFoundException { + //Pre-game setup + var scn = GetScenario(); + + var galley = scn.GetShadowCard("galley"); + var raider = scn.GetShadowCard("raider"); + scn.ShadowMoveCardToHand(raider); + scn.ShadowMoveCardToSupportArea(galley); + + scn.StartGame(); + + scn.FreepsDrawCards(4); + scn.SetTwilight(20); + + scn.FreepsPassCurrentPhaseAction(); + + // Neither 6 tokens nor a raider man: Freeps has initiative + assertEquals(0, scn.GetCultureTokensOn(galley)); + assertEquals(Zone.HAND, raider.getZone()); + assertTrue(scn.FreepsHasInitiative()); + + // 6 tokens, but no raider man: Freeps has initiative + scn.AddTokensToCard(galley, 6); + assertEquals(6, scn.GetCultureTokensOn(galley)); + assertEquals(Zone.HAND, raider.getZone()); + assertTrue(scn.FreepsHasInitiative()); + + // 6 tokens on galley and a raider man: Shadow has initiative + scn.ShadowPlayCard(raider); + assertEquals(6, scn.GetCultureTokensOn(galley)); + assertEquals(Zone.SHADOW_CHARACTERS, raider.getZone()); + assertTrue(scn.ShadowHasInitiative()); + + // 5 tokens on galley and a raider man: Freeps has initiative + scn.RemoveTokensFromCard(galley, 1); + assertEquals(5, scn.GetCultureTokensOn(galley)); + assertEquals(Zone.SHADOW_CHARACTERS, raider.getZone()); + assertTrue(scn.FreepsHasInitiative()); + + // 6 tokens between all raider cards and a raider man: Shadow has initiative + scn.AddTokensToCard(raider, 1); + assertEquals(5, scn.GetCultureTokensOn(galley)); + assertEquals(1, scn.GetCultureTokensOn(raider)); + assertEquals(Zone.SHADOW_CHARACTERS, raider.getZone()); + assertTrue(scn.ShadowHasInitiative()); + } + + @Test + public void RegroupActionAdds1TwilightPerRaiderTokenAndSelfDiscards() throws DecisionResultInvalidException, CardNotFoundException { + //Pre-game setup + var scn = GetScenario(); + + var galley = scn.GetShadowCard("galley"); + var raider = scn.GetShadowCard("raider"); + scn.ShadowMoveCardToHand(raider); + scn.ShadowMoveCardToSupportArea(galley); + + scn.StartGame(); + + scn.FreepsPassCurrentPhaseAction(); + + scn.SkipToPhase(Phase.REGROUP); + + scn.ShadowMoveCharToTable(raider); + scn.AddTokensToCard(raider, 3); + scn.AddTokensToCard(galley, 3); + assertEquals(3, scn.GetTwilight()); + assertEquals(Zone.SUPPORT, galley.getZone()); + + scn.FreepsPassCurrentPhaseAction(); + assertTrue(scn.ShadowActionAvailable(galley)); + scn.ShadowUseCardAction(galley); + assertEquals(9, scn.GetTwilight()); + assertEquals(Zone.DISCARD, galley.getZone()); } }