Playtest updates + bug fixes

- Helpless updated to disable special abilities instead of all text + not usable by enduring nazgul
- Terrible as the Dawn is now unique
- Why Shouldn't I Keep It? now has the shadow player choose the condition to discard
- Updated PC-Movie Playtest to be Multipath.  You can now select FOTR or Towers paths in addition to the existing King path.
- renamed "Special" site path to "Multipath"
- Updated AssignFpCharacterToSkirmish to no longer restrict to only choose or self selectors.  No idea why it's so limited.
- Fixed Guided by One Purpose (V1_31) not actually offering to let the RB exert twice to prevent.
- Added new SendMessage effect that only logs to output.  Added additional player replacement values on top of the existing culture and memory replacements.
This commit is contained in:
Christian 'ketura' McCarty
2023-12-22 01:15:29 -06:00
parent b2759279fc
commit 03ce7fc025
13 changed files with 212 additions and 43 deletions

View File

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

View File

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

View File

@@ -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."
}
]
}
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,7 @@ public class DefaultLotroFormat implements LotroFormat {
private final Map<String,String> _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 {

View File

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