build ValidTgtsDesc via Lang (#9063)

* build ValidTgtsDesc via Lang

* use buildValidDesc for AuraSpells

* Add ValidTgtsDesc to GUI message
This commit is contained in:
Hans Mackowiak
2025-11-02 17:44:54 +01:00
committed by GitHub
parent e33ddee5bf
commit def3fa5d23
11 changed files with 125 additions and 106 deletions

View File

@@ -2,6 +2,8 @@ package forge.util;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.card.CardType;
import forge.util.lang.*; import forge.util.lang.*;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -9,6 +11,7 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
/** /**
* Static library containing language-related utility methods. * Static library containing language-related utility methods.
@@ -216,4 +219,19 @@ public abstract class Lang {
} }
return name.split(" ")[0]; return name.split(" ")[0];
} }
public String buildValidDesc(List<String> valid, boolean multiple) {
return joinHomogenous(valid.stream().map(s -> formatValidDesc(s)).collect(Collectors.toList()), null, multiple ? "and/or" : "or");
}
public String formatValidDesc(String valid) {
List<String> commonStuff = List.of(
//list of common one word non-core type ValidTgts that should be lowercase in the target prompt
"Player", "Opponent", "Card", "Spell", "Permanent"
);
if (commonStuff.contains(valid) || CardType.isACardType(valid)) {
valid = valid.toLowerCase();
}
return valid;
}
} }

View File

@@ -63,7 +63,7 @@ public abstract class CardTraitBase implements GameObject, IHasCardView, IHasSVa
/** Keys of descriptive (text) parameters. */ /** Keys of descriptive (text) parameters. */
private static final ImmutableList<String> descriptiveKeys = ImmutableList.<String>builder() private static final ImmutableList<String> descriptiveKeys = ImmutableList.<String>builder()
.add("Description", "SpellDescription", "StackDescription", "TriggerDescription") .add("Description", "SpellDescription", "StackDescription", "TriggerDescription")
.add("ChangeTypeDesc") .add("ChangeTypeDesc", "ValidTgtsDesc")
.build(); .build();
/** /**

View File

@@ -20,7 +20,6 @@ package forge.game.ability;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.card.CardType;
import forge.game.CardTraitBase; import forge.game.CardTraitBase;
import forge.game.IHasSVars; import forge.game.IHasSVars;
import forge.game.ability.effects.CharmEffect; import forge.game.ability.effects.CharmEffect;
@@ -34,7 +33,6 @@ import forge.util.FileSection;
import io.sentry.Breadcrumb; import io.sentry.Breadcrumb;
import io.sentry.Sentry; import io.sentry.Sentry;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -317,89 +315,8 @@ public final class AbilityFactory {
} }
private static TargetRestrictions readTarget(Map<String, String> mapParams) { private static TargetRestrictions readTarget(Map<String, String> mapParams) {
final String min = mapParams.getOrDefault("TargetMin", "1");
final String max = mapParams.getOrDefault("TargetMax", "1");
// TgtPrompt should only be needed for more complicated ValidTgts // TgtPrompt should only be needed for more complicated ValidTgts
String tgtWhat = mapParams.get("ValidTgts"); return new TargetRestrictions(mapParams);
final String prompt;
if (mapParams.containsKey("TgtPrompt")) {
prompt = mapParams.get("TgtPrompt");
} else if (tgtWhat.equals("Any")) {
prompt = "Select any target";
} else {
final String[] commonStuff = new String[] {
//list of common one word non-core type ValidTgts that should be lowercase in the target prompt
"Player", "Opponent", "Card", "Spell", "Permanent"
};
if (Arrays.asList(commonStuff).contains(tgtWhat) || CardType.CoreType.isValidEnum(tgtWhat)) {
tgtWhat = tgtWhat.toLowerCase();
}
prompt = "Select target " + tgtWhat;
}
TargetRestrictions abTgt = new TargetRestrictions(prompt, mapParams.get("ValidTgts").split(","), min, max);
if (mapParams.containsKey("TgtZone")) {
// if Targeting something not in play, this Key should be set
abTgt.setZone(ZoneType.listValueOf(mapParams.get("TgtZone")));
}
if (mapParams.containsKey("MaxTotalTargetCMC")) {
// only target cards up to a certain total max CMC
abTgt.setMaxTotalCMC(mapParams.get("MaxTotalTargetCMC"));
}
if (mapParams.containsKey("MaxTotalTargetPower")) {
// only target cards up to a certain total max power
abTgt.setMaxTotalPower(mapParams.get("MaxTotalTargetPower"));
}
// TargetValidTargeting most for Counter: e.g. target spell that targets X.
if (mapParams.containsKey("TargetValidTargeting")) {
abTgt.setSAValidTargeting(mapParams.get("TargetValidTargeting"));
}
if (mapParams.containsKey("TargetUnique")) {
abTgt.setUniqueTargets(true);
}
if (mapParams.containsKey("TargetsWithoutSameCreatureType")) {
abTgt.setWithoutSameCreatureType(true);
}
if (mapParams.containsKey("TargetsWithSameCreatureType")) {
abTgt.setWithSameCreatureType(true);
}
if (mapParams.containsKey("TargetsWithSameCardType")) {
abTgt.setWithSameCardType(true);
}
if (mapParams.containsKey("TargetsWithSameController")) {
abTgt.setSameController(true);
}
if (mapParams.containsKey("TargetsWithDifferentControllers")) {
abTgt.setDifferentControllers(true);
}
if (mapParams.containsKey("TargetsForEachPlayer")) {
abTgt.setForEachPlayer(true);
}
if (mapParams.containsKey("TargetsWithDifferentCMC")) {
abTgt.setDifferentCMC(true);
}
if (mapParams.containsKey("TargetsWithDifferentNames")) {
abTgt.setDifferentNames(true);
}
if (mapParams.containsKey("TargetsWithEqualToughness")) {
abTgt.setEqualToughness(true);
}
if (mapParams.containsKey("TargetsAtRandom")) {
abTgt.setRandomTarget(true);
}
if (mapParams.containsKey("RandomNumTargets")) {
abTgt.setRandomNumTargets(true);
}
if (mapParams.containsKey("TargetingPlayer")) {
abTgt.setMandatory(true);
}
return abTgt;
} }
/** /**

View File

@@ -4,7 +4,6 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.card.CardType;
import forge.game.*; import forge.game.*;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
@@ -31,7 +30,6 @@ import org.apache.commons.lang3.tuple.Pair;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
public class ChangeZoneEffect extends SpellAbilityEffect { public class ChangeZoneEffect extends SpellAbilityEffect {
@@ -119,8 +117,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
type = Lang.joinHomogenous(tgts); type = Lang.joinHomogenous(tgts);
defined = true; defined = true;
} else if (sa.hasParam("ChangeType") && !sa.getParam("ChangeType").equals("Card")) { } else if (sa.hasParam("ChangeType") && !sa.getParam("ChangeType").equals("Card")) {
List<String> typeList = Arrays.stream(sa.getParam("ChangeType").split(",")).map(ct -> CardType.isACardType(ct) ? ct.toLowerCase() : ct).collect(Collectors.toList()); type = Lang.getInstance().buildValidDesc(Arrays.asList(sa.getParam("ChangeType").split(",")), num != 1);
type = Lang.joinHomogenous(typeList, null, num == 1 ? "or" : "and/or");
} }
final String cardTag = type.contains("card") ? "" : " card"; final String cardTag = type.contains("card") ? "" : " card";

View File

@@ -2,6 +2,7 @@ package forge.game.ability.effects;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Map;
import forge.card.RemoveType; import forge.card.RemoveType;
import forge.game.Game; import forge.game.Game;
@@ -37,7 +38,7 @@ public class EarthbendEffect extends SpellAbilityEffect {
@Override @Override
public void buildSpellAbility(final SpellAbility sa) { public void buildSpellAbility(final SpellAbility sa) {
TargetRestrictions abTgt = new TargetRestrictions("Select target land you control", "Land.YouCtrl".split(","), "1", "1"); TargetRestrictions abTgt = new TargetRestrictions(Map.of("ValidTgtsDesc", "land you control", "ValidTgts", "Land.YouCtrl"));
sa.setTargetRestrictions(abTgt); sa.setTargetRestrictions(abTgt);
} }

View File

@@ -529,7 +529,7 @@ public class CardState implements GameObject, IHasSVars, ITranslatable {
if (hasSVar("AttachAIValid")) { // TODO combine with AttachAITgts if (hasSVar("AttachAIValid")) { // TODO combine with AttachAITgts
extra += " | AIValid$ " + getSVar("AttachAIValid"); extra += " | AIValid$ " + getSVar("AttachAIValid");
} }
String st = "SP$ Attach | ValidTgts$ Card.CanBeEnchantedBy,Player.CanBeEnchantedBy | TgtZone$ Battlefield,Graveyard | TgtPrompt$ Select target " + desc + extra; String st = "SP$ Attach | ValidTgts$ Card.CanBeEnchantedBy,Player.CanBeEnchantedBy | TgtZone$ Battlefield,Graveyard | ValidTgtsDesc$ " + desc + extra;
auraAbility = AbilityFactory.getAbility(st, this); auraAbility = AbilityFactory.getAbility(st, this);
auraAbility.setIntrinsic(true); auraAbility.setIntrinsic(true);
} }

View File

@@ -1,6 +1,8 @@
package forge.game.keyword; package forge.game.keyword;
import forge.card.CardType; import java.util.Arrays;
import forge.util.Lang;
public class KeywordWithType extends KeywordInstance<KeywordWithType> { public class KeywordWithType extends KeywordInstance<KeywordWithType> {
protected String type = null; protected String type = null;
@@ -31,17 +33,19 @@ public class KeywordWithType extends KeywordInstance<KeywordWithType> {
} }
} else { } else {
descType = type = details; descType = type = details;
boolean multiple = switch(getKeyword()) {
case AFFINITY -> true;
default -> false;
};
descType = Lang.getInstance().buildValidDesc(Arrays.asList(type.split(",")), multiple);
} }
if (CardType.isACardType(descType) || "Permanent".equals(descType) || "Player".equals(descType) || "Opponent".equals(descType)) { if (descType.equalsIgnoreCase("Outlaw")) {
descType = descType.toLowerCase();
} else if (descType.equalsIgnoreCase("Outlaw")) {
reminderType = "Assassin, Mercenary, Pirate, Rogue, and/or Warlock"; reminderType = "Assassin, Mercenary, Pirate, Rogue, and/or Warlock";
} else if (type.equalsIgnoreCase("historic permanent")) { } else if (type.equalsIgnoreCase("historic permanent")) {
reminderType = "artifact, legendary, and/or Saga permanent"; reminderType = "artifact, legendary, and/or Saga permanent";
} } else {
if (reminderType == null) { reminderType = descType;
reminderType = type;
} }
} }

View File

@@ -19,6 +19,7 @@ package forge.game.spellability;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
@@ -31,6 +32,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Lang;
import forge.util.TextUtil; import forge.util.TextUtil;
/** /**
@@ -48,6 +50,7 @@ public class TargetRestrictions {
private String[] originalValidTgts, private String[] originalValidTgts,
validTgts; validTgts;
private String uiPrompt = ""; private String uiPrompt = "";
private String validTgtsDesc = "";
private List<ZoneType> tgtZone = Arrays.asList(ZoneType.Battlefield); private List<ZoneType> tgtZone = Arrays.asList(ZoneType.Battlefield);
// The target SA of this SA must be targeting a Valid X // The target SA of this SA must be targeting a Valid X
@@ -125,12 +128,87 @@ public class TargetRestrictions {
* @param max * @param max
* a {@link java.lang.String} object. * a {@link java.lang.String} object.
*/ */
public TargetRestrictions(final String prompt, final String[] valid, final String min, final String max) { public TargetRestrictions(Map<String, String> mapParams) {
this.uiPrompt = prompt; this.originalValidTgts = mapParams.get("ValidTgts").split(",");
this.originalValidTgts = valid;
this.validTgts = this.originalValidTgts.clone(); this.validTgts = this.originalValidTgts.clone();
this.minTargets = min; this.minTargets = mapParams.getOrDefault("TargetMin", "1");
this.maxTargets = max; this.maxTargets = mapParams.getOrDefault("TargetMax", "1");
if (mapParams.containsKey("ValidTgtsDesc")) {
this.validTgtsDesc = mapParams.get("ValidTgtsDesc");
} else if ("Any".equals(mapParams.get("ValidTgts"))) {
this.validTgtsDesc = "damage target";
} else {
this.validTgtsDesc = Lang.getInstance().buildValidDesc(Arrays.asList(this.validTgts), maxTargets != "1");
}
if (mapParams.containsKey("TgtPrompt")) {
this.uiPrompt = mapParams.get("TgtPrompt");
} else if ("Any".equals(mapParams.get("ValidTgts"))) {
this.uiPrompt = "Select any target";
} else {
this.uiPrompt = "Select target " + validTgtsDesc;
}
if (mapParams.containsKey("TgtZone")) {
// if Targeting something not in play, this Key should be set
setZone(ZoneType.listValueOf(mapParams.get("TgtZone")));
}
if (mapParams.containsKey("MaxTotalTargetCMC")) {
// only target cards up to a certain total max CMC
setMaxTotalCMC(mapParams.get("MaxTotalTargetCMC"));
}
if (mapParams.containsKey("MaxTotalTargetPower")) {
// only target cards up to a certain total max power
setMaxTotalPower(mapParams.get("MaxTotalTargetPower"));
}
// TargetValidTargeting most for Counter: e.g. target spell that targets X.
if (mapParams.containsKey("TargetValidTargeting")) {
setSAValidTargeting(mapParams.get("TargetValidTargeting"));
}
if (mapParams.containsKey("TargetUnique")) {
setUniqueTargets(true);
}
if (mapParams.containsKey("TargetsWithoutSameCreatureType")) {
setWithoutSameCreatureType(true);
}
if (mapParams.containsKey("TargetsWithSameCreatureType")) {
setWithSameCreatureType(true);
}
if (mapParams.containsKey("TargetsWithSameCardType")) {
setWithSameCardType(true);
}
if (mapParams.containsKey("TargetsWithSameController")) {
setSameController(true);
}
if (mapParams.containsKey("TargetsWithDifferentControllers")) {
setDifferentControllers(true);
}
if (mapParams.containsKey("TargetsForEachPlayer")) {
setForEachPlayer(true);
}
if (mapParams.containsKey("TargetsWithDifferentCMC")) {
setDifferentCMC(true);
}
if (mapParams.containsKey("TargetsWithDifferentNames")) {
setDifferentNames(true);
}
if (mapParams.containsKey("TargetsWithEqualToughness")) {
setEqualToughness(true);
}
if (mapParams.containsKey("TargetsAtRandom")) {
setRandomTarget(true);
}
if (mapParams.containsKey("RandomNumTargets")) {
setRandomNumTargets(true);
}
if (mapParams.containsKey("TargetingPlayer")) {
setMandatory(true);
}
} }
public final boolean getMandatory() { public final boolean getMandatory() {
@@ -175,6 +253,10 @@ public class TargetRestrictions {
return this.validTgts; return this.validTgts;
} }
public final String getValidDesc() {
return this.validTgtsDesc;
}
/** /**
* <p> * <p>
* getVTSelection. * getVTSelection.

View File

@@ -1,7 +1,7 @@
Name:Corrupted Roots Name:Corrupted Roots
ManaCost:B ManaCost:B
Types:Enchantment Aura Types:Enchantment Aura
K:Enchant:Land K:Enchant:Forest,Plains
SVar:AttachAILogic:Curse SVar:AttachAILogic:Curse
T:Mode$ Taps | ValidCard$ Card.AttachedBy | TriggerZones$ Battlefield | Execute$ TrigLose | TriggerDescription$ Whenever enchanted land becomes tapped, its controller loses 2 life. T:Mode$ Taps | ValidCard$ Card.AttachedBy | TriggerZones$ Battlefield | Execute$ TrigLose | TriggerDescription$ Whenever enchanted land becomes tapped, its controller loses 2 life.
SVar:TrigLose:DB$ LoseLife | Defined$ TriggeredCardController | LifeAmount$ 2 SVar:TrigLose:DB$ LoseLife | Defined$ TriggeredCardController | LifeAmount$ 2

View File

@@ -1,7 +1,7 @@
Name:Malicious Advice Name:Malicious Advice
ManaCost:X U B ManaCost:X U B
Types:Instant Types:Instant
A:SP$ Tap | TargetMin$ X | TargetMax$ X | ValidTgts$ Artifact,Creature,Land | TgtPrompt$ Select X target artifacts, creatures, or lands | SpellDescription$ Tap X target artifacts, creatures, and/or lands. You lose X life. | SubAbility$ Drain A:SP$ Tap | TargetMin$ X | TargetMax$ X | ValidTgts$ Artifact,Creature,Land | SpellDescription$ Tap X target artifacts, creatures, and/or lands. You lose X life. | SubAbility$ Drain
SVar:Drain:DB$ LoseLife | LifeAmount$ X SVar:Drain:DB$ LoseLife | LifeAmount$ X
SVar:X:Count$xPaid SVar:X:Count$xPaid
AI:RemoveDeck:All AI:RemoveDeck:All

View File

@@ -297,7 +297,7 @@ public final class InputSelectTargets extends InputSyncronizedBase {
} }
if (!choices.contains(card)) { if (!choices.contains(card)) {
showMessage(sa.getHostCard() + " - The selected card is not a valid choice to be targeted."); showMessage(sa.getHostCard() + " - The selected card is not " + Lang.nounWithAmount(1, tgt.getValidDesc()) + ".");
return false; return false;
} }