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