Site Control trigger reworks

- Added SiteLiberated trigger with associated helper classes
- Renamed trigger TakenControlOfSite -> SiteControlled
- Renamed result TakeControlOfSiteResult -> SiteControlledResult
- Renamed trigger enum TAKE_CONTROL_OF_SITE -> CONTROL_SITE
- Implemented V2_1 Driven Into The Hills, which uses a liberate trigger, with associated tests
- Fixed Rapt Hillman liberation not being optional
This commit is contained in:
Christian 'ketura' McCarty
2024-06-02 15:32:22 -05:00
parent e4493e227a
commit 4ee23c86a0
14 changed files with 255 additions and 65 deletions

View File

@@ -1164,41 +1164,46 @@
vitality: 2
site: 4
effects: [
{
type: trigger
trigger: {
type: startOfPhase
phase: regroup
}
effect: {
type: preventable
player: shadow
text: Would you like to spot 3 Men to prevent liberating site?
cost: {
type: spot
filter: man
count: 3
}
effect: {
type: liberateSite
}
}
}
{
type: activated
phase: maneuver
requires: {
type: canSpot
filter: another,culture(men),man
}
cost: {
type: exert
select: self
}
effect: {
type: takeControlOfSite
}
}
{
type: trigger
trigger: {
type: startOfPhase
phase: regroup
}
effect: {
type: Optional
player: freeps
text: Would you like to liberate a site?
effect: {
type: preventable
player: shadow
text: Would you like to spot 3 Men to prevent liberating site?
cost: {
type: spot
filter: man
count: 3
}
effect: {
type: liberateSite
}
}
}
}
{
type: activated
phase: maneuver
requires: {
type: canSpot
filter: another,culture(men),man
}
cost: {
type: exert
select: self
}
effect: {
type: takeControlOfSite
}
}
]
gametext: At the start of the regroup phase, the Free People's player may liberate a site. You may spot 3 Men to prevent this.<br><b>Maneuver:</b> Spot another [men] Man and exert this minion to take control of a site.
lore: Dunlendings take delight in destruction.

View File

@@ -1493,7 +1493,7 @@
type: trigger
optional: true
trigger: {
type: takenControlOfSite
type: SiteControlled
}
requires: {
type: canSpot

View File

@@ -22,18 +22,39 @@
twilight: 1
type: Condition
keywords: Support Area
/*requires: {
}
effects: [
{
type: ToPlay
requires: {
type: CanSpot
filter: culture(Dunland), minion
}
}
{
type: Modifier
modifier: {
type: SkipPhase
phase: Archery
requires: {
type: HaveInitiative
side: Shadow
}
}
}
]*/
gametext: To play, spot a [Dunland] minion.<br>While the shadow has initiative, skip the archery phase.<br>If the Free-Peoples player liberates a site, discard this condition.
{
type: trigger
optional: false
trigger: {
type: SiteLiberated
}
effect: {
type: Discard
select: self
}
}
]
gametext: To play, spot a [Dunland] minion.<br>While the shadow has initiative, skip the archery phase.<br>When the Free-Peoples player liberates a site, discard this condition.
lore: "'Not in half a thousand years have they forgotten their grievance that the lords of Gondor gave the Mark to Eorl the Young and made alliance with him.'"
promotext: ""
alts: {

View File

@@ -7,10 +7,10 @@ import com.gempukku.lotro.cards.build.PlayerSource;
import com.gempukku.lotro.cards.build.field.FieldUtils;
import com.gempukku.lotro.cards.build.field.effect.appender.resolver.PlayerResolver;
import com.gempukku.lotro.logic.timing.EffectResult;
import com.gempukku.lotro.logic.timing.results.TakeControlOfSiteResult;
import com.gempukku.lotro.logic.timing.results.SiteControlledResult;
import org.json.simple.JSONObject;
public class TakenControlOfSite implements TriggerCheckerProducer {
public class SiteControlled implements TriggerCheckerProducer {
@Override
public TriggerChecker getTriggerChecker(JSONObject value, CardGenerationEnvironment environment) throws InvalidCardDefinitionException {
FieldUtils.validateAllowedFields(value, "player");
@@ -23,8 +23,8 @@ public class TakenControlOfSite implements TriggerCheckerProducer {
public boolean accepts(ActionContext actionContext) {
String playerId = playerSource.getPlayer(actionContext);
EffectResult effectResult = actionContext.getEffectResult();
if (effectResult.getType() == EffectResult.Type.TAKE_CONTROL_OF_SITE) {
TakeControlOfSiteResult takeResult = (TakeControlOfSiteResult) effectResult;
if (effectResult.getType() == EffectResult.Type.CONTROL_SITE) {
SiteControlledResult takeResult = (SiteControlledResult) effectResult;
return takeResult.getPlayerId().equals(playerId);
}
return false;

View File

@@ -0,0 +1,32 @@
package com.gempukku.lotro.cards.build.field.effect.trigger;
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.PlayerSource;
import com.gempukku.lotro.cards.build.field.FieldUtils;
import com.gempukku.lotro.cards.build.field.effect.appender.resolver.PlayerResolver;
import com.gempukku.lotro.logic.timing.EffectResult;
import com.gempukku.lotro.logic.timing.results.SiteControlledResult;
import com.gempukku.lotro.logic.timing.results.SiteLiberatedResult;
import org.json.simple.JSONObject;
public class SiteLiberated implements TriggerCheckerProducer {
@Override
public TriggerChecker getTriggerChecker(JSONObject value, CardGenerationEnvironment environment) throws InvalidCardDefinitionException {
FieldUtils.validateAllowedFields(value);
return new TriggerChecker() {
@Override
public boolean accepts(ActionContext actionContext) {
EffectResult effectResult = actionContext.getEffectResult();
return effectResult.getType() == EffectResult.Type.LIBERATE_SITE;
}
@Override
public boolean isBefore() {
return false;
}
};
}
}

View File

@@ -65,9 +65,10 @@ public class TriggerCheckerFactory {
triggerCheckers.put("replacessite", new ReplacesSite());
triggerCheckers.put("revealscardfromtopofdrawdeck", new RevealsCardFromTopOfDrawDeck());
triggerCheckers.put("revealedcardfromhand", new RevealedCardFromHand());
triggerCheckers.put("siteliberated", new SiteLiberated());
triggerCheckers.put("sitecontrolled", new SiteControlled());
triggerCheckers.put("skirmishabouttoend", new SkirmishAboutToEnd());
triggerCheckers.put("startofskirmishinvolving", new StartOfSkirmishInvolving());
triggerCheckers.put("takencontrolofsite", new TakenControlOfSite());
triggerCheckers.put("takesoffring", new TakesOffRing());
triggerCheckers.put("takeswound", new TakesWound());
triggerCheckers.put("transferred", new Transferred());

View File

@@ -8,6 +8,7 @@ import com.gempukku.lotro.logic.timing.AbstractEffect;
import com.gempukku.lotro.logic.timing.Effect;
import com.gempukku.lotro.logic.timing.PlayConditions;
import com.gempukku.lotro.logic.timing.results.DiscardCardsFromPlayResult;
import com.gempukku.lotro.logic.timing.results.SiteLiberatedResult;
import java.util.HashSet;
import java.util.List;
@@ -85,6 +86,7 @@ public class LiberateASiteEffect extends AbstractEffect {
game.getGameState().sendMessage(_liberatingPlayer + " liberated a " + GameUtils.getCardLink(siteToLiberate) + " using " + GameUtils.getCardLink(_source));
game.getActionsEnvironment().emitEffectResult(new SiteLiberatedResult(_liberatingPlayer));
liberatedSiteCallback(siteToLiberate);
return new FullEffectResult(true);

View File

@@ -7,7 +7,7 @@ import com.gempukku.lotro.logic.GameUtils;
import com.gempukku.lotro.logic.timing.AbstractEffect;
import com.gempukku.lotro.logic.timing.Effect;
import com.gempukku.lotro.logic.timing.Preventable;
import com.gempukku.lotro.logic.timing.results.TakeControlOfSiteResult;
import com.gempukku.lotro.logic.timing.results.SiteControlledResult;
public class TakeControlOfASiteEffect extends AbstractEffect implements Preventable {
private final PhysicalCard _source;
@@ -69,7 +69,7 @@ public class TakeControlOfASiteEffect extends AbstractEffect implements Preventa
if (site != null && !_prevented) {
game.getGameState().takeControlOfCard(_playerId, game, site, Zone.SUPPORT);
game.getGameState().sendMessage(_playerId + " took control of " + GameUtils.getCardLink(site));
game.getActionsEnvironment().emitEffectResult(new TakeControlOfSiteResult(_playerId));
game.getActionsEnvironment().emitEffectResult(new SiteControlledResult(_playerId));
return new FullEffectResult(true);
}
return new FullEffectResult(false);

View File

@@ -56,7 +56,7 @@ public abstract class EffectResult {
ASSIGNED_AGAINST, ASSIGNED_TO_SKIRMISH, CARD_TRANSFERRED,
REPLACE_SITE, TAKE_CONTROL_OF_SITE,
REPLACE_SITE, CONTROL_SITE, LIBERATE_SITE,
FINISHED_PLAYING_FELLOWSHIP,

View File

@@ -2,11 +2,11 @@ package com.gempukku.lotro.logic.timing.results;
import com.gempukku.lotro.logic.timing.EffectResult;
public class TakeControlOfSiteResult extends EffectResult {
public class SiteControlledResult extends EffectResult {
private final String _playerId;
public TakeControlOfSiteResult(String playerId) {
super(Type.TAKE_CONTROL_OF_SITE);
public SiteControlledResult(String playerId) {
super(Type.CONTROL_SITE);
_playerId = playerId;
}

View File

@@ -0,0 +1,16 @@
package com.gempukku.lotro.logic.timing.results;
import com.gempukku.lotro.logic.timing.EffectResult;
public class SiteLiberatedResult extends EffectResult {
private final String _playerId;
public SiteLiberatedResult(String playerId) {
super(Type.LIBERATE_SITE);
_playerId = playerId;
}
public String getPlayerId() {
return _playerId;
}
}

View File

@@ -37,7 +37,7 @@ public class WinConditionRule {
game.playerWon(game.getGameState().getCurrentPlayerId(), "Surviving to Regroup phase on site 9");
}
} else if (game.getFormat().winOnControlling5Sites()
&& effectResults.getType() == EffectResult.Type.TAKE_CONTROL_OF_SITE) {
&& effectResults.getType() == EffectResult.Type.CONTROL_SITE) {
for (String opponent : GameUtils.getShadowPlayers(game)) {
if (Filters.countActive(game, CardType.SITE, Filters.siteControlled(opponent)) >= 5)
game.playerWon(opponent, "Controls 5 sites");

View File

@@ -956,6 +956,10 @@ public class GenericCardTestHelper extends AbstractAtTest {
}
public Boolean IsSiteControlled(PhysicalCardImpl card) {
return card.getCardController() != null;
}
public void ApplyAdHocModifier(Modifier mod)

View File

@@ -2,9 +2,12 @@ package com.gempukku.lotro.cards.unofficial.pc.vsets.set_v02;
import com.gempukku.lotro.cards.GenericCardTestHelper;
import com.gempukku.lotro.common.*;
import com.gempukku.lotro.filters.Filters;
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.AddKeywordModifier;
import com.gempukku.lotro.logic.modifiers.HasInitiativeModifier;
import org.junit.Test;
import java.util.HashMap;
@@ -18,8 +21,12 @@ public class Card_V2_001_Tests
return new GenericCardTestHelper(
new HashMap<>()
{{
put("card", "102_1");
// put other cards in here as needed for the test case
put("driven", "102_1");
put("madman", "4_12");
put("freca", "9_2");
put("hillman", "15_90");
put("runner", "1_178");
}},
GenericCardTestHelper.FellowshipSites,
GenericCardTestHelper.FOTRFrodo,
@@ -46,7 +53,7 @@ public class Card_V2_001_Tests
var scn = GetScenario();
var card = scn.GetFreepsCard("card");
var card = scn.GetFreepsCard("driven");
assertEquals("Driven into the Hills", card.getBlueprint().getTitle());
assertNull(card.getBlueprint().getSubtitle());
@@ -58,18 +65,120 @@ public class Card_V2_001_Tests
assertEquals(1, card.getBlueprint().getTwilightCost());
}
// Uncomment any @Test markers below once this is ready to be used
//@Test
public void DrivenintotheHillsTest1() throws DecisionResultInvalidException, CardNotFoundException {
@Test
public void DrivenRequiresDunlandMinionToPlay() throws DecisionResultInvalidException, CardNotFoundException {
//Pre-game setup
var scn = GetScenario();
var card = scn.GetFreepsCard("card");
scn.FreepsMoveCardToHand(card);
var driven = scn.GetShadowCard("driven");
var runner = scn.GetShadowCard("runner");
var madman = scn.GetShadowCard("madman");
scn.ShadowMoveCardToHand(driven, runner, madman);
scn.StartGame();
scn.FreepsPlayCard(card);
assertEquals(1, scn.GetTwilight());
scn.SetTwilight(50);
scn.FreepsPassCurrentPhaseAction();
assertFalse(scn.ShadowPlayAvailable(driven));
scn.ShadowPlayCard(runner);
assertFalse(scn.ShadowPlayAvailable(driven));
scn.ShadowPlayCard(madman);
assertTrue(scn.ShadowPlayAvailable(driven));
}
@Test
public void DrivenSkipsArcheryPhaseIfShadowHasInitiative() throws DecisionResultInvalidException, CardNotFoundException {
//Pre-game setup
var scn = GetScenario();
var driven = scn.GetShadowCard("driven");
var madman = scn.GetShadowCard("madman");
scn.ShadowMoveCardToSupportArea(driven);
scn.ShadowMoveCharToTable(madman);
scn.StartGame();
//cheating to give Shadow initiative
scn.ApplyAdHocModifier(new HasInitiativeModifier(null, null, Side.SHADOW));
scn.SkipToPhase(Phase.MANEUVER);
assertTrue(scn.ShadowHasInitiative());
assertEquals(Phase.MANEUVER, scn.GetCurrentPhase());
scn.PassCurrentPhaseActions();
//Skipped archery entirely
assertEquals(Phase.ASSIGNMENT, scn.GetCurrentPhase());
}
@Test
public void DrivenDoesNotSkipsArcheryPhaseIfFreepsHasInitiative() throws DecisionResultInvalidException, CardNotFoundException {
//Pre-game setup
var scn = GetScenario();
var driven = scn.GetShadowCard("driven");
var madman = scn.GetShadowCard("madman");
scn.ShadowMoveCardToSupportArea(driven);
scn.ShadowMoveCharToTable(madman);
scn.StartGame();
//cheating to give Freeps initiative, since there are too few cards in hand
scn.ApplyAdHocModifier(new HasInitiativeModifier(null, null, Side.FREE_PEOPLE));
scn.SkipToPhase(Phase.MANEUVER);
assertFalse(scn.ShadowHasInitiative());
assertEquals(Phase.MANEUVER, scn.GetCurrentPhase());
scn.PassCurrentPhaseActions();
//Did not skip archery
assertEquals(Phase.ARCHERY, scn.GetCurrentPhase());
}
@Test
public void DrivenSelfDiscardsWhenASiteIsLiberated() throws DecisionResultInvalidException, CardNotFoundException {
//Pre-game setup
var scn = GetScenario();
var driven = scn.GetShadowCard("driven");
var madman = scn.GetShadowCard("madman");
var hillman = scn.GetShadowCard("hillman");
var freca = scn.GetShadowCard("freca");
scn.ShadowMoveCardToSupportArea(driven);
var site1 = scn.GetFreepsSite(1);
scn.StartGame();
//Need to have sites controlled to liberate
scn.SkipToSite(3);
scn.ShadowMoveCharToTable(hillman, madman);
scn.ShadowMoveCardToHand(freca);
scn.FreepsPassCurrentPhaseAction();
scn.ShadowPlayCard(freca);
scn.ShadowAcceptOptionalTrigger();
assertTrue(scn.IsSiteControlled(site1));
scn.SkipToPhase(Phase.MANEUVER);
assertTrue(scn.IsSiteControlled(site1));
assertEquals(Zone.SUPPORT, driven.getZone());
scn.SkipToPhase(Phase.REGROUP);
//Rapt Hillman gives Freeps the opportunity to liberate a site,
// and shadow can stop it (but we choose not to)
scn.FreepsAcceptOptionalTrigger();
scn.ShadowChooseNo();
assertFalse(scn.IsSiteControlled(site1));
assertEquals(Zone.DISCARD, driven.getZone());
}
}