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.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() {

View File

@@ -2453,17 +2453,8 @@ public class Card extends GameEntity implements Comparable<Card>, 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")
@@ -7171,51 +7162,62 @@ public class Card extends GameEntity implements Comparable<Card>, 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) {

View File

@@ -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;
}

View File

@@ -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(":");

View File

@@ -3,38 +3,50 @@ package forge.game.keyword;
import forge.card.CardType;
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
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);
}
}

View File

@@ -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) {

View File

@@ -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)) {