From 4d2b634e4fbd09446dbca7d4a90bb5199edfb88e Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 28 Sep 2025 15:35:19 +0200 Subject: [PATCH] cantBeEnchantedByMsg and cantAttach StaticAbility (#8772) * cantBeEnchantedByMsg and cantAttach StaticAbility * finish cantBeAttachedMsg --- .../src/main/java/forge/game/GameEntity.java | 91 ++++++++++++------- .../src/main/java/forge/game/card/Card.java | 78 ++++++++-------- .../main/java/forge/game/card/CardState.java | 12 +-- .../main/java/forge/game/keyword/Equip.java | 2 + .../forge/game/keyword/KeywordWithType.java | 44 +++++---- .../StaticAbilityCantAttach.java | 6 +- .../match/input/InputSelectTargets.java | 9 +- 7 files changed, 139 insertions(+), 103 deletions(-) diff --git a/forge-game/src/main/java/forge/game/GameEntity.java b/forge-game/src/main/java/forge/game/GameEntity.java index 7e51c00234f..1447b1d78ad 100644 --- a/forge-game/src/main/java/forge/game/GameEntity.java +++ b/forge-game/src/main/java/forge/game/GameEntity.java @@ -36,12 +36,15 @@ import forge.game.card.CardPredicates; import forge.game.card.CounterType; import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; +import forge.game.keyword.KeywordWithType; import forge.game.player.Player; import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementType; import forge.game.spellability.SpellAbility; +import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbilityCantAttach; import forge.game.zone.ZoneType; +import forge.util.Lang; public abstract class GameEntity extends GameObject implements IIdentifiable { protected int id; @@ -218,63 +221,83 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { return canBeAttached(attach, sa, false); } public boolean canBeAttached(final Card attach, SpellAbility sa, boolean checkSBA) { - // master mode - if (!attach.isAttachment() || (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE)) - || equals(attach)) { - return false; + return cantBeAttachedMsg(attach, sa, checkSBA) == null; + } + + public String cantBeAttachedMsg(final Card attach, SpellAbility sa) { + return cantBeAttachedMsg(attach, sa, false); + } + public String cantBeAttachedMsg(final Card attach, SpellAbility sa, boolean checkSBA) { + if (!attach.isAttachment()) { + return attach.getName() + " is not an attachment"; + } + if (equals(attach)) { + return attach.getName() + " can't attach to itself"; + } + + if (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE)) { + return attach.getName() + " is a creature without reconfigure"; } if (attach.isPhasedOut()) { - return false; + return attach.getName() + " is phased out"; } - // check for rules - if (attach.isAura() && !canBeEnchantedBy(attach)) { - return false; + if (attach.isAura()) { + String msg = cantBeEnchantedByMsg(attach); + if (msg != null) { + return msg; + } } - if (attach.isEquipment() && !canBeEquippedBy(attach, sa)) { - return false; + if (attach.isEquipment()) { + String msg = cantBeEquippedByMsg(attach, sa); + if (msg != null) { + return msg; + } } - if (attach.isFortification() && !canBeFortifiedBy(attach)) { - return false; + if (attach.isFortification()) { + String msg = cantBeFortifiedByMsg(attach); + if (msg != null) { + return msg; + } } - // check for can't attach static - if (StaticAbilityCantAttach.cantAttach(this, attach, checkSBA)) { - return false; + StaticAbility stAb = StaticAbilityCantAttach.cantAttach(this, attach, checkSBA); + if (stAb != null) { + return stAb.toString(); } - // true for all - return true; + return null; } - protected boolean canBeEquippedBy(final Card aura, SpellAbility sa) { - /** - * Equip only to Creatures which are cards - */ - return false; - } - - protected boolean canBeFortifiedBy(final Card aura) { + protected String cantBeEquippedByMsg(final Card aura, SpellAbility sa) { /** * Equip only to Lands which are cards */ - return false; + return getName() + " is not a Creature"; } - protected boolean canBeEnchantedBy(final Card aura) { + protected String cantBeFortifiedByMsg(final Card fort) { + /** + * Equip only to Lands which are cards + */ + return getName() + " is not a Land"; + } + + protected String cantBeEnchantedByMsg(final Card aura) { if (!aura.hasKeyword(Keyword.ENCHANT)) { - return false; + return "No Enchant Keyword"; } for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) { - String k = ki.getOriginal(); - String m[] = k.split(":"); - String v = m[1]; - if (!isValid(v.split(","), aura.getController(), aura, null)) { - return false; + if (ki instanceof KeywordWithType kwt) { + String v = kwt.getValidType(); + String desc = kwt.getTypeDescription(); + if (!isValid(v.split(","), aura.getController(), aura, null)) { + return getName() + " is not " + Lang.nounWithAmount(1, desc); + } } } - return true; + return null; } public boolean hasCounters() { diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 37722d54e81..49894c670aa 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -2453,17 +2453,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr } else if (keyword.startsWith("DeckLimit")) { final String[] k = keyword.split(":"); sbLong.append(k[2]).append("\r\n"); - } else if (keyword.startsWith("Enchant")) { - String m[] = keyword.split(":"); - String desc; - if (m.length > 2) { - desc = m[2]; - } else { - desc = m[1]; - if (CardType.isACardType(desc) || "Permanent".equals(desc) || "Player".equals(desc) || "Opponent".equals(desc)) { - desc = desc.toLowerCase(); - } - } + } else if (keyword.startsWith("Enchant") && inst instanceof KeywordWithType kwt) { + String desc = kwt.getTypeDescription(); sbLong.append("Enchant ").append(desc).append("\r\n"); } else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph") || keyword.startsWith("Disguise") || keyword.startsWith("Reflect") @@ -3797,7 +3788,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr public final void addLeavesPlayCommand(final GameCommand c) { leavePlayCommandList.add(c); } - + public void addStaticCommandList(Object[] objects) { staticCommandList.add(objects); } @@ -4812,7 +4803,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr public void addDraftAction(String s) { draftActions.add(s); } - + private int intensity = 0; public final void addIntensity(final int n) { intensity += n; @@ -7171,51 +7162,62 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr } @Override - protected final boolean canBeEnchantedBy(final Card aura) { + protected final String cantBeEnchantedByMsg(final Card aura) { if (!aura.hasKeyword(Keyword.ENCHANT)) { - return false; + return "No Enchant Keyword"; } for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) { - String k = ki.getOriginal(); - String m[] = k.split(":"); - String v = m[1]; - if (!isValid(v.split(","), aura.getController(), aura, null)) { - return false; - } - if (!v.contains("inZone") && !isInPlay()) { - return false; + if (ki instanceof KeywordWithType kwt) { + String v = kwt.getValidType(); + String desc = kwt.getTypeDescription(); + if (!isValid(v.split(","), aura.getController(), aura, null) || (!v.contains("inZone") && !isInPlay())) { + return getName() + " is not " + Lang.nounWithAmount(1, desc); + } } } - return true; + return null; } + @Override - protected final boolean canBeEquippedBy(final Card equip, SpellAbility sa) { + protected String cantBeEquippedByMsg(final Card equip, SpellAbility sa) { if (!isInPlay()) { - return false; + return getName() + " is not in play"; } if (sa != null && sa.isEquip()) { - return isValid(sa.getTargetRestrictions().getValidTgts(), sa.getActivatingPlayer(), equip, sa); + if (!isValid(sa.getTargetRestrictions().getValidTgts(), sa.getActivatingPlayer(), equip, sa)) { + Equip eq = (Equip) sa.getKeyword(); + return getName() + " is not " + Lang.nounWithAmount(1, eq.getValidDescription()); + } + return null; } - return isCreature(); + if (!isCreature()) { + return getName() + " is not a creature"; + } + return null; } @Override - protected boolean canBeFortifiedBy(final Card fort) { - return isLand() && isInPlay() && !fort.isLand(); + protected String cantBeFortifiedByMsg(final Card fort) { + if (!isLand()) { + return getName() + " is not a Land"; + } + if (!isInPlay()) { + return getName() + " is not in play"; + } + if (fort.isLand()) { + return fort.getName() + " is a Land"; + } + + return null; } - /* (non-Javadoc) - * @see forge.game.GameEntity#canBeAttached(forge.game.card.Card, boolean) - */ @Override - public boolean canBeAttached(Card attach, SpellAbility sa, boolean checkSBA) { - // phase check there + public String cantBeAttachedMsg(final Card attach, SpellAbility sa, boolean checkSBA) { if (isPhasedOut() && !attach.isPhasedOut()) { - return false; + return getName() + " is phased out"; } - - return super.canBeAttached(attach, sa, checkSBA); + return super.cantBeAttachedMsg(attach, sa, checkSBA); } public final boolean canBeSacrificedBy(final SpellAbility source, final boolean effect) { 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 57f9cf39896..289520ecf28 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -32,6 +32,7 @@ import forge.game.card.CardView.CardStateView; import forge.game.keyword.Keyword; import forge.game.keyword.KeywordCollection; import forge.game.keyword.KeywordInterface; +import forge.game.keyword.KeywordWithType; import forge.game.player.Player; import forge.game.replacement.ReplacementEffect; import forge.game.spellability.LandAbility; @@ -501,15 +502,8 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable { String desc = ""; String extra = ""; for (KeywordInterface ki : this.getCachedKeyword(Keyword.ENCHANT)) { - String o = ki.getOriginal(); - String m[] = o.split(":"); - if (m.length > 2) { - desc = m[2]; - } else { - desc = m[1]; - if (CardType.isACardType(desc) || "Permanent".equals(desc) || "Player".equals(desc) || "Opponent".equals(desc)) { - desc = desc.toLowerCase(); - } + if (ki instanceof KeywordWithType kwt) { + desc = kwt.getTypeDescription(); } break; } diff --git a/forge-game/src/main/java/forge/game/keyword/Equip.java b/forge-game/src/main/java/forge/game/keyword/Equip.java index 66a5c2ad65d..bf7313943f1 100644 --- a/forge-game/src/main/java/forge/game/keyword/Equip.java +++ b/forge-game/src/main/java/forge/game/keyword/Equip.java @@ -7,6 +7,8 @@ public class Equip extends KeywordWithCost { public Equip() { } + public String getValidDescription() { return type; } + @Override protected void parse(String details) { String[] k = details.split(":"); 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 a31ace72781..2bf274f1272 100644 --- a/forge-game/src/main/java/forge/game/keyword/KeywordWithType.java +++ b/forge-game/src/main/java/forge/game/keyword/KeywordWithType.java @@ -3,38 +3,50 @@ package forge.game.keyword; import forge.card.CardType; public class KeywordWithType extends KeywordInstance { - protected String type; + protected String type = null; + protected String descType = null; + protected String reminderType = null; + + public String getValidType() { return type; } + public String getTypeDescription() { return descType; } @Override protected void parse(String details) { - if (CardType.isACardType(details)) { - type = details.toLowerCase(); - } else if (details.contains(":")) { + String k[]; + if (details.contains(":")) { switch (getKeyword()) { case AFFINITY: - type = details.split(":")[1]; - // type lists defined by rules should not be changed by TextChange in reminder text - if (type.equalsIgnoreCase("Outlaw")) { - type = "Assassin, Mercenary, Pirate, Rogue, and/or Warlock"; - } else if (type.equalsIgnoreCase("historic permanent")) { - type = "artifact, legendary, and/or Saga permanent"; - } - break; case BANDSWITH: + case ENCHANT: case HEXPROOF: case LANDWALK: - type = details.split(":")[1]; + k = details.split(":"); + type = k[0]; + descType = k[1]; break; default: - type = details.split(":")[0]; + k = details.split(":"); + type = k[1]; + descType = k[0]; } } else { - type = details; + descType = type = details; + } + + if (CardType.isACardType(descType) || "Permanent".equals(descType) || "Player".equals(descType) || "Opponent".equals(descType)) { + descType = descType.toLowerCase(); + } else 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; } } @Override protected String formatReminderText(String reminderText) { - return String.format(reminderText, type); + return String.format(reminderText, reminderType); } } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttach.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttach.java index 8b2ba2bda27..70704f3ac95 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttach.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttach.java @@ -6,7 +6,7 @@ import forge.game.zone.ZoneType; public class StaticAbilityCantAttach { - public static boolean cantAttach(final GameEntity target, final Card card, boolean checkSBA) { + public static StaticAbility cantAttach(final GameEntity target, final Card card, boolean checkSBA) { // CantTarget static abilities for (final Card ca : target.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { for (final StaticAbility stAb : ca.getStaticAbilities()) { @@ -15,11 +15,11 @@ public class StaticAbilityCantAttach { } if (applyCantAttachAbility(stAb, card, target, checkSBA)) { - return true; + return stAb; } } } - return false; + return null; } public static boolean applyCantAttachAbility(final StaticAbility stAb, final Card card, final GameEntity target, boolean checkSBA) { 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 95b09fa716e..6861b922ddd 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 @@ -169,9 +169,12 @@ public final class InputSelectTargets extends InputSyncronizedBase { // TODO should use sa.canTarget(card) instead? // it doesn't have messages - if (sa.isSpell() && sa.getHostCard().isAura() && !card.canBeAttached(sa.getHostCard(), sa)) { - showMessage(sa.getHostCard() + " - Cannot enchant this card (Shroud? Protection? Restrictions?)."); - return false; + if (sa.isSpell() && sa.getHostCard().isAura()) { + String msg = card.cantBeAttachedMsg(sa.getHostCard(), sa); + if (msg != null) { + showMessage(sa.getHostCard() + " - " + msg); + return false; + } } //If the card is not a valid target if (!card.canBeTargetedBy(sa)) {