cantBeEnchantedByMsg and cantAttach StaticAbility (#8772)

* cantBeEnchantedByMsg and cantAttach StaticAbility

* finish cantBeAttachedMsg
This commit is contained in:
Hans Mackowiak
2025-09-28 15:35:19 +02:00
committed by GitHub
parent 4eccfa8177
commit 4d2b634e4f
7 changed files with 139 additions and 103 deletions

View File

@@ -36,12 +36,15 @@ import forge.game.card.CardPredicates;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface; import forge.game.keyword.KeywordInterface;
import forge.game.keyword.KeywordWithType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttach; import forge.game.staticability.StaticAbilityCantAttach;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Lang;
public abstract class GameEntity extends GameObject implements IIdentifiable { public abstract class GameEntity extends GameObject implements IIdentifiable {
protected int id; protected int id;
@@ -218,63 +221,83 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
return canBeAttached(attach, sa, false); return canBeAttached(attach, sa, false);
} }
public boolean canBeAttached(final Card attach, SpellAbility sa, boolean checkSBA) { public boolean canBeAttached(final Card attach, SpellAbility sa, boolean checkSBA) {
// master mode return cantBeAttachedMsg(attach, sa, checkSBA) == null;
if (!attach.isAttachment() || (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE)) }
|| equals(attach)) {
return false; 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()) { if (attach.isPhasedOut()) {
return false; return attach.getName() + " is phased out";
} }
// check for rules if (attach.isAura()) {
if (attach.isAura() && !canBeEnchantedBy(attach)) { String msg = cantBeEnchantedByMsg(attach);
return false; if (msg != null) {
return msg;
}
} }
if (attach.isEquipment() && !canBeEquippedBy(attach, sa)) { if (attach.isEquipment()) {
return false; String msg = cantBeEquippedByMsg(attach, sa);
if (msg != null) {
return msg;
}
} }
if (attach.isFortification() && !canBeFortifiedBy(attach)) { if (attach.isFortification()) {
return false; String msg = cantBeFortifiedByMsg(attach);
if (msg != null) {
return msg;
}
} }
// check for can't attach static StaticAbility stAb = StaticAbilityCantAttach.cantAttach(this, attach, checkSBA);
if (StaticAbilityCantAttach.cantAttach(this, attach, checkSBA)) { if (stAb != null) {
return false; return stAb.toString();
} }
// true for all return null;
return true;
} }
protected boolean canBeEquippedBy(final Card aura, SpellAbility sa) { protected String cantBeEquippedByMsg(final Card aura, SpellAbility sa) {
/**
* Equip only to Creatures which are cards
*/
return false;
}
protected boolean canBeFortifiedBy(final Card aura) {
/** /**
* Equip only to Lands which are cards * 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)) { if (!aura.hasKeyword(Keyword.ENCHANT)) {
return false; return "No Enchant Keyword";
} }
for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) { for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) {
String k = ki.getOriginal(); if (ki instanceof KeywordWithType kwt) {
String m[] = k.split(":"); String v = kwt.getValidType();
String v = m[1]; String desc = kwt.getTypeDescription();
if (!isValid(v.split(","), aura.getController(), aura, null)) { if (!isValid(v.split(","), aura.getController(), aura, null)) {
return false; return getName() + " is not " + Lang.nounWithAmount(1, desc);
}
} }
} }
return true; return null;
} }
public boolean hasCounters() { public boolean hasCounters() {

View File

@@ -2453,17 +2453,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
} else if (keyword.startsWith("DeckLimit")) { } else if (keyword.startsWith("DeckLimit")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
sbLong.append(k[2]).append("\r\n"); sbLong.append(k[2]).append("\r\n");
} else if (keyword.startsWith("Enchant")) { } else if (keyword.startsWith("Enchant") && inst instanceof KeywordWithType kwt) {
String m[] = keyword.split(":"); String desc = kwt.getTypeDescription();
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();
}
}
sbLong.append("Enchant ").append(desc).append("\r\n"); sbLong.append("Enchant ").append(desc).append("\r\n");
} else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph") } else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph")
|| keyword.startsWith("Disguise") || keyword.startsWith("Reflect") || keyword.startsWith("Disguise") || keyword.startsWith("Reflect")
@@ -3797,7 +3788,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
public final void addLeavesPlayCommand(final GameCommand c) { public final void addLeavesPlayCommand(final GameCommand c) {
leavePlayCommandList.add(c); leavePlayCommandList.add(c);
} }
public void addStaticCommandList(Object[] objects) { public void addStaticCommandList(Object[] objects) {
staticCommandList.add(objects); staticCommandList.add(objects);
} }
@@ -4812,7 +4803,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
public void addDraftAction(String s) { public void addDraftAction(String s) {
draftActions.add(s); draftActions.add(s);
} }
private int intensity = 0; private int intensity = 0;
public final void addIntensity(final int n) { public final void addIntensity(final int n) {
intensity += n; intensity += n;
@@ -7171,51 +7162,62 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
} }
@Override @Override
protected final boolean canBeEnchantedBy(final Card aura) { protected final String cantBeEnchantedByMsg(final Card aura) {
if (!aura.hasKeyword(Keyword.ENCHANT)) { if (!aura.hasKeyword(Keyword.ENCHANT)) {
return false; return "No Enchant Keyword";
} }
for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) { for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) {
String k = ki.getOriginal(); if (ki instanceof KeywordWithType kwt) {
String m[] = k.split(":"); String v = kwt.getValidType();
String v = m[1]; String desc = kwt.getTypeDescription();
if (!isValid(v.split(","), aura.getController(), aura, null)) { if (!isValid(v.split(","), aura.getController(), aura, null) || (!v.contains("inZone") && !isInPlay())) {
return false; return getName() + " is not " + Lang.nounWithAmount(1, desc);
} }
if (!v.contains("inZone") && !isInPlay()) {
return false;
} }
} }
return true; return null;
} }
@Override @Override
protected final boolean canBeEquippedBy(final Card equip, SpellAbility sa) { protected String cantBeEquippedByMsg(final Card equip, SpellAbility sa) {
if (!isInPlay()) { if (!isInPlay()) {
return false; return getName() + " is not in play";
} }
if (sa != null && sa.isEquip()) { 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 @Override
protected boolean canBeFortifiedBy(final Card fort) { protected String cantBeFortifiedByMsg(final Card fort) {
return isLand() && isInPlay() && !fort.isLand(); 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 @Override
public boolean canBeAttached(Card attach, SpellAbility sa, boolean checkSBA) { public String cantBeAttachedMsg(final Card attach, SpellAbility sa, boolean checkSBA) {
// phase check there
if (isPhasedOut() && !attach.isPhasedOut()) { if (isPhasedOut() && !attach.isPhasedOut()) {
return false; return getName() + " is phased out";
} }
return super.cantBeAttachedMsg(attach, sa, checkSBA);
return super.canBeAttached(attach, sa, checkSBA);
} }
public final boolean canBeSacrificedBy(final SpellAbility source, final boolean effect) { public final boolean canBeSacrificedBy(final SpellAbility source, final boolean effect) {

View File

@@ -32,6 +32,7 @@ import forge.game.card.CardView.CardStateView;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordCollection; import forge.game.keyword.KeywordCollection;
import forge.game.keyword.KeywordInterface; import forge.game.keyword.KeywordInterface;
import forge.game.keyword.KeywordWithType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.LandAbility; import forge.game.spellability.LandAbility;
@@ -501,15 +502,8 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
String desc = ""; String desc = "";
String extra = ""; String extra = "";
for (KeywordInterface ki : this.getCachedKeyword(Keyword.ENCHANT)) { for (KeywordInterface ki : this.getCachedKeyword(Keyword.ENCHANT)) {
String o = ki.getOriginal(); if (ki instanceof KeywordWithType kwt) {
String m[] = o.split(":"); desc = kwt.getTypeDescription();
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();
}
} }
break; break;
} }

View File

@@ -7,6 +7,8 @@ public class Equip extends KeywordWithCost {
public Equip() { public Equip() {
} }
public String getValidDescription() { return type; }
@Override @Override
protected void parse(String details) { protected void parse(String details) {
String[] k = details.split(":"); String[] k = details.split(":");

View File

@@ -3,38 +3,50 @@ package forge.game.keyword;
import forge.card.CardType; import forge.card.CardType;
public class KeywordWithType extends KeywordInstance<KeywordWithType> { public class KeywordWithType extends KeywordInstance<KeywordWithType> {
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 @Override
protected void parse(String details) { protected void parse(String details) {
if (CardType.isACardType(details)) { String k[];
type = details.toLowerCase(); if (details.contains(":")) {
} else if (details.contains(":")) {
switch (getKeyword()) { switch (getKeyword()) {
case AFFINITY: 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 BANDSWITH:
case ENCHANT:
case HEXPROOF: case HEXPROOF:
case LANDWALK: case LANDWALK:
type = details.split(":")[1]; k = details.split(":");
type = k[0];
descType = k[1];
break; break;
default: default:
type = details.split(":")[0]; k = details.split(":");
type = k[1];
descType = k[0];
} }
} else { } 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 @Override
protected String formatReminderText(String reminderText) { protected String formatReminderText(String reminderText) {
return String.format(reminderText, type); return String.format(reminderText, reminderType);
} }
} }

View File

@@ -6,7 +6,7 @@ import forge.game.zone.ZoneType;
public class StaticAbilityCantAttach { 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 // CantTarget static abilities
for (final Card ca : target.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { for (final Card ca : target.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) { for (final StaticAbility stAb : ca.getStaticAbilities()) {
@@ -15,11 +15,11 @@ public class StaticAbilityCantAttach {
} }
if (applyCantAttachAbility(stAb, card, target, checkSBA)) { 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) { public static boolean applyCantAttachAbility(final StaticAbility stAb, final Card card, final GameEntity target, boolean checkSBA) {

View File

@@ -169,9 +169,12 @@ public final class InputSelectTargets extends InputSyncronizedBase {
// TODO should use sa.canTarget(card) instead? // TODO should use sa.canTarget(card) instead?
// it doesn't have messages // it doesn't have messages
if (sa.isSpell() && sa.getHostCard().isAura() && !card.canBeAttached(sa.getHostCard(), sa)) { if (sa.isSpell() && sa.getHostCard().isAura()) {
showMessage(sa.getHostCard() + " - Cannot enchant this card (Shroud? Protection? Restrictions?)."); String msg = card.cantBeAttachedMsg(sa.getHostCard(), sa);
return false; if (msg != null) {
showMessage(sa.getHostCard() + " - " + msg);
return false;
}
} }
//If the card is not a valid target //If the card is not a valid target
if (!card.canBeTargetedBy(sa)) { if (!card.canBeTargetedBy(sa)) {