From def3fa5d2311ceb73b396808138da78e2c7095fe Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 2 Nov 2025 17:44:54 +0100 Subject: [PATCH] build ValidTgtsDesc via Lang (#9063) * build ValidTgtsDesc via Lang * use buildValidDesc for AuraSpells * Add ValidTgtsDesc to GUI message --- forge-core/src/main/java/forge/util/Lang.java | 18 ++++ .../main/java/forge/game/CardTraitBase.java | 2 +- .../forge/game/ability/AbilityFactory.java | 85 +---------------- .../ability/effects/ChangeZoneEffect.java | 5 +- .../game/ability/effects/EarthbendEffect.java | 3 +- .../main/java/forge/game/card/CardState.java | 2 +- .../forge/game/keyword/KeywordWithType.java | 18 ++-- .../game/spellability/TargetRestrictions.java | 92 ++++++++++++++++++- .../res/cardsfolder/c/corrupted_roots.txt | 2 +- .../res/cardsfolder/m/malicious_advice.txt | 2 +- .../match/input/InputSelectTargets.java | 2 +- 11 files changed, 125 insertions(+), 106 deletions(-) diff --git a/forge-core/src/main/java/forge/util/Lang.java b/forge-core/src/main/java/forge/util/Lang.java index 67f2cf1bccd..219bee6b16e 100644 --- a/forge-core/src/main/java/forge/util/Lang.java +++ b/forge-core/src/main/java/forge/util/Lang.java @@ -2,6 +2,8 @@ package forge.util; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; + +import forge.card.CardType; import forge.util.lang.*; import org.apache.commons.lang3.StringUtils; @@ -9,6 +11,7 @@ import java.util.Collection; import java.util.List; import java.util.function.Function; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Static library containing language-related utility methods. @@ -216,4 +219,19 @@ public abstract class Lang { } return name.split(" ")[0]; } + + public String buildValidDesc(List 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 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; + } } diff --git a/forge-game/src/main/java/forge/game/CardTraitBase.java b/forge-game/src/main/java/forge/game/CardTraitBase.java index 109d007a1ae..ff253dd111e 100644 --- a/forge-game/src/main/java/forge/game/CardTraitBase.java +++ b/forge-game/src/main/java/forge/game/CardTraitBase.java @@ -63,7 +63,7 @@ public abstract class CardTraitBase implements GameObject, IHasCardView, IHasSVa /** Keys of descriptive (text) parameters. */ private static final ImmutableList descriptiveKeys = ImmutableList.builder() .add("Description", "SpellDescription", "StackDescription", "TriggerDescription") - .add("ChangeTypeDesc") + .add("ChangeTypeDesc", "ValidTgtsDesc") .build(); /** diff --git a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java index f0c89aa25d2..04a596de7c7 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java @@ -20,7 +20,6 @@ package forge.game.ability; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.card.CardStateName; -import forge.card.CardType; import forge.game.CardTraitBase; import forge.game.IHasSVars; import forge.game.ability.effects.CharmEffect; @@ -34,7 +33,6 @@ import forge.util.FileSection; import io.sentry.Breadcrumb; import io.sentry.Sentry; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -317,89 +315,8 @@ public final class AbilityFactory { } private static TargetRestrictions readTarget(Map mapParams) { - final String min = mapParams.getOrDefault("TargetMin", "1"); - final String max = mapParams.getOrDefault("TargetMax", "1"); - // TgtPrompt should only be needed for more complicated ValidTgts - String tgtWhat = mapParams.get("ValidTgts"); - 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; + return new TargetRestrictions(mapParams); } /** diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java index 6577532f699..00d296dc59d 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java @@ -4,7 +4,6 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.card.CardStateName; -import forge.card.CardType; import forge.game.*; import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityKey; @@ -31,7 +30,6 @@ import org.apache.commons.lang3.tuple.Pair; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; public class ChangeZoneEffect extends SpellAbilityEffect { @@ -119,8 +117,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { type = Lang.joinHomogenous(tgts); defined = true; } else if (sa.hasParam("ChangeType") && !sa.getParam("ChangeType").equals("Card")) { - List typeList = Arrays.stream(sa.getParam("ChangeType").split(",")).map(ct -> CardType.isACardType(ct) ? ct.toLowerCase() : ct).collect(Collectors.toList()); - type = Lang.joinHomogenous(typeList, null, num == 1 ? "or" : "and/or"); + type = Lang.getInstance().buildValidDesc(Arrays.asList(sa.getParam("ChangeType").split(",")), num != 1); } final String cardTag = type.contains("card") ? "" : " card"; diff --git a/forge-game/src/main/java/forge/game/ability/effects/EarthbendEffect.java b/forge-game/src/main/java/forge/game/ability/effects/EarthbendEffect.java index 723bf4522e9..ea7219d3cde 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/EarthbendEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/EarthbendEffect.java @@ -2,6 +2,7 @@ package forge.game.ability.effects; import java.util.Arrays; import java.util.EnumSet; +import java.util.Map; import forge.card.RemoveType; import forge.game.Game; @@ -37,7 +38,7 @@ public class EarthbendEffect extends SpellAbilityEffect { @Override 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); } diff --git a/forge-game/src/main/java/forge/game/card/CardState.java b/forge-game/src/main/java/forge/game/card/CardState.java index 8080402c16d..549421395e6 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -529,7 +529,7 @@ public class CardState implements GameObject, IHasSVars, ITranslatable { if (hasSVar("AttachAIValid")) { // TODO combine with AttachAITgts 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.setIntrinsic(true); } diff --git a/forge-game/src/main/java/forge/game/keyword/KeywordWithType.java b/forge-game/src/main/java/forge/game/keyword/KeywordWithType.java index 2bf274f1272..902edff6e20 100644 --- a/forge-game/src/main/java/forge/game/keyword/KeywordWithType.java +++ b/forge-game/src/main/java/forge/game/keyword/KeywordWithType.java @@ -1,6 +1,8 @@ package forge.game.keyword; -import forge.card.CardType; +import java.util.Arrays; + +import forge.util.Lang; public class KeywordWithType extends KeywordInstance { protected String type = null; @@ -31,17 +33,19 @@ public class KeywordWithType extends KeywordInstance { } } else { 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)) { - descType = descType.toLowerCase(); - } else if (descType.equalsIgnoreCase("Outlaw")) { + if (descType.equalsIgnoreCase("Outlaw")) { reminderType = "Assassin, Mercenary, Pirate, Rogue, and/or Warlock"; } else if (type.equalsIgnoreCase("historic permanent")) { reminderType = "artifact, legendary, and/or Saga permanent"; - } - if (reminderType == null) { - reminderType = type; + } else { + reminderType = descType; } } diff --git a/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java b/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java index 5f1713723d5..c07853dba64 100644 --- a/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java +++ b/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java @@ -19,6 +19,7 @@ package forge.game.spellability; import java.util.Arrays; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.ObjectUtils; @@ -31,6 +32,7 @@ import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.player.Player; import forge.game.zone.ZoneType; +import forge.util.Lang; import forge.util.TextUtil; /** @@ -48,6 +50,7 @@ public class TargetRestrictions { private String[] originalValidTgts, validTgts; private String uiPrompt = ""; + private String validTgtsDesc = ""; private List tgtZone = Arrays.asList(ZoneType.Battlefield); // The target SA of this SA must be targeting a Valid X @@ -125,12 +128,87 @@ public class TargetRestrictions { * @param max * a {@link java.lang.String} object. */ - public TargetRestrictions(final String prompt, final String[] valid, final String min, final String max) { - this.uiPrompt = prompt; - this.originalValidTgts = valid; + public TargetRestrictions(Map mapParams) { + this.originalValidTgts = mapParams.get("ValidTgts").split(","); this.validTgts = this.originalValidTgts.clone(); - this.minTargets = min; - this.maxTargets = max; + this.minTargets = mapParams.getOrDefault("TargetMin", "1"); + 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() { @@ -175,6 +253,10 @@ public class TargetRestrictions { return this.validTgts; } + public final String getValidDesc() { + return this.validTgtsDesc; + } + /** *

* getVTSelection. diff --git a/forge-gui/res/cardsfolder/c/corrupted_roots.txt b/forge-gui/res/cardsfolder/c/corrupted_roots.txt index fab9aae21df..141cfb78b36 100644 --- a/forge-gui/res/cardsfolder/c/corrupted_roots.txt +++ b/forge-gui/res/cardsfolder/c/corrupted_roots.txt @@ -1,7 +1,7 @@ Name:Corrupted Roots ManaCost:B Types:Enchantment Aura -K:Enchant:Land +K:Enchant:Forest,Plains SVar:AttachAILogic:Curse 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 diff --git a/forge-gui/res/cardsfolder/m/malicious_advice.txt b/forge-gui/res/cardsfolder/m/malicious_advice.txt index 2063fd3bcf0..b19f6b80ba1 100644 --- a/forge-gui/res/cardsfolder/m/malicious_advice.txt +++ b/forge-gui/res/cardsfolder/m/malicious_advice.txt @@ -1,7 +1,7 @@ Name:Malicious Advice ManaCost:X U B 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:X:Count$xPaid AI:RemoveDeck:All diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectTargets.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectTargets.java index 6861b922ddd..22d60f67e02 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectTargets.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectTargets.java @@ -297,7 +297,7 @@ public final class InputSelectTargets extends InputSyncronizedBase { } 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; }