diff --git a/gemp-lotr/gemp-lotr-cards/src/main/resources/blueprintMapping.txt b/gemp-lotr/gemp-lotr-cards/src/main/resources/blueprintMapping.txt index 0be1809b0..4122a408e 100644 --- a/gemp-lotr/gemp-lotr-cards/src/main/resources/blueprintMapping.txt +++ b/gemp-lotr/gemp-lotr-cards/src/main/resources/blueprintMapping.txt @@ -360,14 +360,14 @@ gl_theOneRing,1_2 9_58,9_35 #Gollum, Dark as Darkness FA 9_59,9_28 -#2023 WC - Goblin Runner FA -1_386,1_178 #2023 WC Top 8 - Goblin Runner FA 1_383,1_178 #2023 WC World Challenger - Goblin Runner FA 1_384,1_178 #2023 WC World Champion- Goblin Runner FA 1_385,1_178 +#2023 WC - Goblin Runner FA +1_386,1_178 #Aragorn,Elessar Telcontar FA #10_127,10_25 #Arwen, Queen of Elves and Men FA diff --git a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/yuletide/set02-Ringwraith-errata-playtest.hjson b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/yuletide/set02-Ringwraith-errata-playtest.hjson index 522098eb5..62828878c 100644 --- a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/yuletide/set02-Ringwraith-errata-playtest.hjson +++ b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/yuletide/set02-Ringwraith-errata-playtest.hjson @@ -35,7 +35,7 @@ { type: modifier modifier: { - type: DisableGameText + type: cantUseSpecialAbilities filter: bearer } } @@ -44,7 +44,7 @@ phase: maneuver cost: { type: exert - filter: choose(Nazgul) + filter: choose(Nazgul,not(enduring)) } effect: { type: Transfer diff --git a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/yuletide/set03-Moria-errata-playtest.hjson b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/yuletide/set03-Moria-errata-playtest.hjson index 883fc818f..9a9ff45a3 100644 --- a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/yuletide/set03-Moria-errata-playtest.hjson +++ b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/yuletide/set03-Moria-errata-playtest.hjson @@ -28,7 +28,7 @@ unique: false side: Shadow culture: Moria - twilight: 0 + twilight: 1 type: Condition target: unbound,companion,not(hasAttached(name(Such a Little Thing))) effects: [ @@ -51,24 +51,35 @@ filter: bearer memory: bearer } + { + type: ChooseActiveCards + text: Choose a possession attached to {bearer} to discard. + filter: choose(possession,AttachedTo(bearer)) + memorize: chosenPoss + } { type: choice player: freeps texts: [ - Discard an item from {bearer} - Wound {bearer} + Exert {bearer} to prevent discarding {chosenPoss} + Discard another Free Peoples item (artifact or possession) to prevent discarding {chosenPoss} + Let {chosenPoss} get discarded ] effects: [ { - type: Discard - player: freeps - filter: choose(item,AttachedTo(bearer)) - } - { - type: wound + type: exert filter: bearer times: 1 } + { + type: Discard + player: freeps + filter: choose(item,side(freeps),not(memory(chosenPoss))) + } + { + type: SendMessage + text: "{freeps} chooses to not prevent {chosenPoss} getting discarded." + } ] } ] diff --git a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/yuletide/set03-Sauron-errata-playtest.hjson b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/yuletide/set03-Sauron-errata-playtest.hjson index a5704de68..75c349683 100644 --- a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/yuletide/set03-Sauron-errata-playtest.hjson +++ b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/errata/yuletide/set03-Sauron-errata-playtest.hjson @@ -25,7 +25,7 @@ style: Standard } title: Terrible as the Dawn - unique: false + unique: true side: Shadow culture: Sauron twilight: 2 @@ -116,7 +116,7 @@ unique: false side: Shadow culture: Sauron - twilight: 2 + twilight: 1 type: Condition target: or(companion,ally) requires: { @@ -141,7 +141,7 @@ effects: [ { type: Discard - player: freeps + player: shadow filter: choose(side(freeps),condition) } { diff --git a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/setV01/V1_moria.hjson b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/setV01/V1_moria.hjson index 8ef481446..1cbf6f129 100644 --- a/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/setV01/V1_moria.hjson +++ b/gemp-lotr/gemp-lotr-cards/src/main/resources/cards/unofficial/pc/setV01/V1_moria.hjson @@ -30,11 +30,29 @@ type: discard filter: self } - effect: { - type: assignFpCharacterToSkirmish - fpCharacter: choose(ringbearer) - against: choose(culture(moria),creature) - } + effect: [ + { + type: ChooseActiveCards + text: Choose a {moria} creature to assign to the Ring-bearer. + filter: choose(culture(moria),creature) + memorize: chosenCreature + } + { + type: Preventable + player: freeps + text: Would you like to exert the Ring-bearer twice to prevent {chosenCreature} being assigned to them? + cost: { + type: Exert + filter: choose(Ring-bearer) + times: 2 + } + effect: { + type: assignFpCharacterToSkirmish + fpCharacter: choose(ringbearer) + against: memory(chosenCreature) + } + } + ] } ] } diff --git a/gemp-lotr/gemp-lotr-cards/src/main/resources/lotrFormats.hjson b/gemp-lotr/gemp-lotr-cards/src/main/resources/lotrFormats.hjson index 7c710225b..1240d1b19 100644 --- a/gemp-lotr/gemp-lotr-cards/src/main/resources/lotrFormats.hjson +++ b/gemp-lotr/gemp-lotr-cards/src/main/resources/lotrFormats.hjson @@ -2,7 +2,7 @@ { name: Debug code: debug - sites: SPECIAL + sites: MULTIPATH maximumSameName: 100 set: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 101, 151 @@ -368,7 +368,7 @@ name: Pre-Shadows Multipath code: multipath order: 27 - sites: SPECIAL + sites: MULTIPATH banned: [ "1_138", "1_311", "1_316", "1_331", "1_338", "1_354", "1_355", "1_360", "2_14", "2_75", "3_1", "3_113", "4_73", "4_338", "4_355", "4_357", "7_190", @@ -503,7 +503,7 @@ name: Anything Goes code: rev_tow_sta order: 37 - sites: SPECIAL + sites: MULTIPATH cancelRingBearerSkirmish: true set: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 30, 31, 32, 33 @@ -592,7 +592,7 @@ name: PLAYTEST - Movie Block (PC) code: test_pc_movie order: 42 - sites: KING + sites: MULTIPATH winAtEndOfRegroup: true discardPileIsPublic: true hall: true @@ -748,7 +748,7 @@ { name: Limited - Multipath Movie code: limited_multipath - sites: SPECIAL + sites: MULTIPATH cancelRingBearerSkirmish: false maximumSameName: 100 set: [ diff --git a/gemp-lotr/gemp-lotr-common/src/main/java/com/gempukku/lotro/common/SitesBlock.java b/gemp-lotr/gemp-lotr-common/src/main/java/com/gempukku/lotro/common/SitesBlock.java index 212ff116b..8ef14cc4e 100644 --- a/gemp-lotr/gemp-lotr-common/src/main/java/com/gempukku/lotro/common/SitesBlock.java +++ b/gemp-lotr/gemp-lotr-common/src/main/java/com/gempukku/lotro/common/SitesBlock.java @@ -2,7 +2,7 @@ package com.gempukku.lotro.common; public enum SitesBlock { FELLOWSHIP("Fellowship"), TWO_TOWERS("Towers"), KING("King"), - SHADOWS("Shadows and onwards"), SPECIAL("Special"), + SHADOWS("Shadows and onwards"), MULTIPATH("Multipath"), SECOND_ED("2nd edition"), //Additional Hobbit Draft block diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/EffectAppenderFactory.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/EffectAppenderFactory.java index b67be0c4c..ffe93de98 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/EffectAppenderFactory.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/EffectAppenderFactory.java @@ -135,6 +135,7 @@ public class EffectAppenderFactory { effectAppenderProducers.put("revealhand", new RevealHand()); effectAppenderProducers.put("revealrandomcardsfromhand", new RevealRandomCardsFromHand()); effectAppenderProducers.put("revealtopcardsofdrawdeck", new RevealTopCardsOfDrawDeck()); + effectAppenderProducers.put("sendmessage", new SendMessage()); effectAppenderProducers.put("shadowcanthaveinitiative", new ShadowCantHaveInitiative()); effectAppenderProducers.put("shufflecardsfromdiscardintodrawdeck", new ShuffleCardsFromDiscardIntoDrawDeck()); effectAppenderProducers.put("shufflecardsfromhandintodrawdeck", new ShuffleCardsFromHandIntoDrawDeck()); diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/AssignFpCharacterToSkirmish.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/AssignFpCharacterToSkirmish.java index 3f50b0c9d..612d28c2f 100644 --- a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/AssignFpCharacterToSkirmish.java +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/AssignFpCharacterToSkirmish.java @@ -69,11 +69,16 @@ public class AssignFpCharacterToSkirmish implements EffectAppenderProducer { return result; } + private FilterableSource getSource(String filter, CardGenerationEnvironment environment) throws InvalidCardDefinitionException { if (filter.startsWith("choose(") && filter.endsWith(")")) return environment.getFilterFactory().generateFilter(filter.substring(filter.indexOf("(") + 1, filter.lastIndexOf(")")), environment); if (filter.equals("self")) return ActionContext::getSource; - throw new InvalidCardDefinitionException("The selectors for this effect have to be of 'choose' type or 'self'"); + + return environment.getFilterFactory().generateFilter(filter, environment); + //What exactly was the point of this? Pulling from other sources should be just fine. + //I wish I knew what bug this was addressing. + //throw new InvalidCardDefinitionException("The selectors for this effect have to be of 'choose' type or 'self'"); } } diff --git a/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/SendMessage.java b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/SendMessage.java new file mode 100644 index 000000000..35d98ab9d --- /dev/null +++ b/gemp-lotr/gemp-lotr-logic/src/main/java/com/gempukku/lotro/cards/build/field/effect/appender/SendMessage.java @@ -0,0 +1,55 @@ +package com.gempukku.lotro.cards.build.field.effect.appender; + +import com.gempukku.lotro.cards.build.ActionContext; +import com.gempukku.lotro.cards.build.CardGenerationEnvironment; +import com.gempukku.lotro.cards.build.InvalidCardDefinitionException; +import com.gempukku.lotro.cards.build.ValueSource; +import com.gempukku.lotro.cards.build.field.FieldUtils; +import com.gempukku.lotro.cards.build.field.effect.EffectAppender; +import com.gempukku.lotro.cards.build.field.effect.EffectAppenderProducer; +import com.gempukku.lotro.cards.build.field.effect.appender.resolver.CardResolver; +import com.gempukku.lotro.cards.build.field.effect.appender.resolver.ValueResolver; +import com.gempukku.lotro.filters.Filters; +import com.gempukku.lotro.game.PhysicalCard; +import com.gempukku.lotro.game.state.LotroGame; +import com.gempukku.lotro.logic.GameUtils; +import com.gempukku.lotro.logic.actions.CostToEffectAction; +import com.gempukku.lotro.logic.effects.ExertCharactersEffect; +import com.gempukku.lotro.logic.timing.AbstractSuccessfulEffect; +import com.gempukku.lotro.logic.timing.Effect; +import com.gempukku.lotro.logic.timing.UnrespondableEffect; +import org.json.simple.JSONObject; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +public class SendMessage implements EffectAppenderProducer { + @Override + public EffectAppender createEffectAppender(JSONObject effectObject, CardGenerationEnvironment environment) throws InvalidCardDefinitionException { + FieldUtils.validateAllowedFields(effectObject, "text"); + + final String text = FieldUtils.getString(effectObject.get("text"), "text"); + if (text == null) + throw new InvalidCardDefinitionException("SendMessage requires the text field to be set."); + + return new DelayedAppender() { + @Override + protected Effect createEffect(boolean cost, CostToEffectAction action, ActionContext actionContext) { + return new UnrespondableEffect() { + + @Override + protected void doPlayEffect(LotroGame game) { + game.getGameState().sendMessage(GameUtils.SubstituteText(text, actionContext)); + } + }; + } + + @Override + public boolean isPlayableInFull(ActionContext actionContext) { + return true; + } + }; + } + +} 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 1457abc76..2d3e6b196 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 @@ -73,6 +73,10 @@ public class GameUtils { return shadowPlayers.toArray(new String[shadowPlayers.size()]); } + public static String getFirstOpponent(LotroGame game, String playerId) { + return Arrays.stream(getOpponents(game, playerId)).findFirst().orElse(null); + } + public static String[] getAllPlayers(LotroGame game) { final GameState gameState = game.getGameState(); final PlayerOrder playerOrder = gameState.getPlayerOrder(); @@ -189,23 +193,60 @@ public class GameUtils { while (result.contains("{")) { int startIndex = result.indexOf("{"); int endIndex = result.indexOf("}"); - String memory = result.substring(startIndex + 1, endIndex); + String memoryName = result.substring(startIndex + 1, endIndex); + String memory = memoryName.toLowerCase(); + String found = null; String culture = getCultureImage(memory); if(culture != null) { - result = result.replace("{" + memory + "}", culture); + found = culture; } else if(context != null){ - String cardNames = GameUtils.getAppendedNames(context.getCardsFromMemory(memory)); - if(cardNames.equalsIgnoreCase("none")) { - try { - cardNames = context.getValueFromMemory(memory); - } - catch(IllegalArgumentException ex) { - cardNames = "none"; + if(memory.equals("you")) { + found = context.getPerformingPlayer(); + if(found == null) { + found = "nobody"; + } + } + else if(memory.equals("owner")) { + var source = context.getSource(); + if(source != null) { + found = source.getOwner(); + } + if(found == null) { + found = "nobody"; + } + } + else if(memory.equals("opponent")) { + found = getFirstOpponent(context.getGame(), context.getSource().getOwner()); + if(found == null) { + found = "nobody"; + } + } + else if(memory.equals("freeps") || memory.equals("free peoples")) { + found = getFreePeoplePlayer(context.getGame()); + if(found == null) { + found = "nobody"; + } + } + else if(memory.equals("shadow")) { + found = getFirstShadowPlayer(context.getGame()); + } + else { + found = GameUtils.getAppendedNames(context.getCardsFromMemory(memory)); + if(found.equalsIgnoreCase("none")) { + try { + found = context.getValueFromMemory(memory); + } + catch(IllegalArgumentException ex) { + found = "NONE"; + } } } - result = result.replace("{" + memory + "}", cardNames); } + if(found == null) { + found = "NOTHING"; + } + result = result.replace("{" + memoryName + "}", found); } return result; diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/game/formats/DefaultLotroFormat.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/game/formats/DefaultLotroFormat.java index 294743d57..e538b07ca 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/game/formats/DefaultLotroFormat.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/game/formats/DefaultLotroFormat.java @@ -41,7 +41,7 @@ public class DefaultLotroFormat implements LotroFormat { private final Map _errataCardMap = new TreeMap<>(); public DefaultLotroFormat(AdventureLibrary adventureLibrary, LotroCardBlueprintLibrary library, JSONDefs.Format def) throws InvalidPropertiesFormatException{ - this(library, adventureLibrary.getAdventure(def.adventure), def.name, def.code, def.order, def.surveyUrl, SitesBlock.valueOf(def.sites), + this(library, adventureLibrary.getAdventure(def.adventure), def.name, def.code, def.order, def.surveyUrl, SitesBlock.findBlock(def.sites), def.validateShadowFPCount, def.minimumDeckSize, def.maximumSameName, def.mulliganRule, def.cancelRingBearerSkirmish, def.ruleOfFour, def.winAtEndOfRegroup, def.discardPileIsPublic, def.winOnControlling5Sites, def.playtest, def.hall); @@ -611,10 +611,10 @@ public class DefaultLotroFormat implements LotroFormat { if (siteBlueprint.getCardType() != CardType.SITE) { result += "Card assigned as Site is not really a site.\n"; } - else if (siteBlueprint.getSiteBlock() != _siteBlock && _siteBlock != SitesBlock.SPECIAL) { + else if (siteBlueprint.getSiteBlock() != _siteBlock && _siteBlock != SitesBlock.MULTIPATH) { result += "Site does not match block: " + GameUtils.getFullName(siteBlueprint) + "\n"; } - else if (_siteBlock == SitesBlock.SPECIAL && siteBlueprint.getSiteBlock() == SitesBlock.SHADOWS) { + else if (_siteBlock == SitesBlock.MULTIPATH && siteBlueprint.getSiteBlock() == SitesBlock.SHADOWS) { result += "Post-Shadows site not allowed: " + GameUtils.getFullName(siteBlueprint) + "\n"; } else { @@ -630,7 +630,7 @@ public class DefaultLotroFormat implements LotroFormat { } } - if (_siteBlock == SitesBlock.SPECIAL) { + if (_siteBlock == SitesBlock.MULTIPATH) { SitesBlock usedBlock = null; for (String site : deck.getSites()) { try { diff --git a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/unofficial/pc/vsets/set_v01/Card_V1_031_Tests.java b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/unofficial/pc/vsets/set_v01/Card_V1_031_Tests.java index 696cc11d5..940dbf468 100644 --- a/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/unofficial/pc/vsets/set_v01/Card_V1_031_Tests.java +++ b/gemp-lotr/gemp-lotr-server/src/test/java/com/gempukku/lotro/cards/unofficial/pc/vsets/set_v01/Card_V1_031_Tests.java @@ -112,7 +112,7 @@ public class Card_V1_031_Tests } @Test - public void PurposeSpots4StackedTentaclesToAssignToRB() throws DecisionResultInvalidException, CardNotFoundException { + public void PurposeSpots4StackedTentaclesToAssignToRBIfPreventionDeclined() throws DecisionResultInvalidException, CardNotFoundException { //Pre-game setup GenericCardTestHelper scn = GetScenario(); @@ -137,8 +137,46 @@ public class Card_V1_031_Tests scn.FreepsPassCurrentPhaseAction(); assertTrue(scn.ShadowActionAvailable("One Purpose")); scn.ShadowUseCardAction(purpose); + + scn.FreepsChooseNo(); assertTrue(scn.IsCharAssigned(scn.GetRingBearer())); //should now see One Purpose and the four tentacles stacked on it in the discard pile assertEquals(5, scn.GetShadowDiscardCount()); } + + @Test + public void PurposeRBAssignmentCanBePreventedByExertingRBTwice() throws DecisionResultInvalidException, CardNotFoundException { + //Pre-game setup + GenericCardTestHelper scn = GetScenario(); + + var frodo = scn.GetRingBearer(); + PhysicalCardImpl merry = scn.GetFreepsCard("merry"); + PhysicalCardImpl aragorn = scn.GetFreepsCard("aragorn"); + scn.FreepsMoveCharToTable(merry, aragorn); + + PhysicalCardImpl purpose = scn.GetShadowCard("purpose"); + PhysicalCardImpl htentacle = scn.GetShadowCard("htentacle"); + scn.ShadowMoveCardToHand(htentacle); + scn.ShadowMoveCardToSupportArea(purpose); + scn.StackCardsOn(purpose,scn.GetShadowCard("ftentacle1"), scn.GetShadowCard("ftentacle2"), scn.GetShadowCard("ftentacle3"), scn.GetShadowCard("ftentacle4")); + + scn.StartGame(); + scn.SetTwilight(4); + scn.ApplyAdHocModifier(new KeywordModifier(null, Filters.siteNumber(2), Keyword.MARSH)); + scn.FreepsPassCurrentPhaseAction(); + scn.ShadowPlayCard(htentacle); + scn.ShadowDeclineOptionalTrigger(); + + scn.SkipToPhase(Phase.ASSIGNMENT); + scn.FreepsPassCurrentPhaseAction(); + assertTrue(scn.ShadowActionAvailable("One Purpose")); + scn.ShadowUseCardAction(purpose); + + assertEquals(4, scn.GetVitality(frodo)); + scn.FreepsChooseYes(); + assertEquals(2, scn.GetVitality(frodo)); + assertFalse(scn.IsCharAssigned(scn.GetRingBearer())); + //should now see One Purpose and the four tentacles stacked on it in the discard pile + assertEquals(5, scn.GetShadowDiscardCount()); + } }