Corsair War Galley errata + tests

This commit is contained in:
Christian 'ketura' McCarty
2023-06-04 22:23:47 -05:00
parent d5d1071a42
commit 5a5acbe9e5
8 changed files with 308 additions and 36 deletions

View File

@@ -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.<br>While you can spot 6 [raider] tokens and a [raider] Man, the Shadow has initiative, regardless of the Free Peoples player's hand. <br><b>Regroup:</b> 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: {
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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<Culture> cultures = new HashSet<>();
for (PhysicalCard physicalCard : Filters.filterActive(game, filters)) {

View File

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

View File

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

View File

@@ -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<String, String>()
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 players hand. <br><b>Regroup:</b> 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());
}
}