From 9f0a8353bc7300b6ad3719f8ae8b953fa34258c2 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Tue, 27 Jan 2026 07:36:22 +0100 Subject: [PATCH 1/3] Keywords now implement ICardTraitChanges --- .../src/main/java/forge/game/card/Card.java | 8 ++--- .../forge/game/keyword/KeywordInstance.java | 34 +++++++++++++++++-- .../forge/game/keyword/KeywordInterface.java | 5 ++- .../forge/game/keyword/KeywordsChange.java | 17 ++++------ 4 files changed, 46 insertions(+), 18 deletions(-) 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 f3d00d79d82..e6538f56852 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -3481,7 +3481,7 @@ public void updateSpellAbilities(List list, CardState state) { // keywords should already been cleanup by layers for (KeywordInterface kw : getUnhiddenKeywords(state)) { - list.addAll(kw.getAbilities()); + kw.applySpellAbility(list); } } @@ -7101,7 +7101,7 @@ public void updateStaticAbilities(List list, CardState state) { // keywords are already sorted by Layer for (KeywordInterface kw : getUnhiddenKeywords(state)) { - list.addAll(kw.getStaticAbilities()); + kw.applyStaticAbility(list); } } @@ -7140,7 +7140,7 @@ public void updateTriggers(List list, CardState state) { // Keywords are already sorted by Layer for (KeywordInterface kw : getUnhiddenKeywords(state)) { - list.addAll(kw.getTriggers()); + kw.applyTrigger(list); } } @@ -7160,7 +7160,7 @@ public void updateReplacementEffects(List list, CardState sta // Keywords are already sorted by Layer for (KeywordInterface kw : getUnhiddenKeywords(state)) { - list.addAll(kw.getReplacements()); + kw.applyReplacementEffect(list); } // Shield Counter aren't affected by Changed Card Traits diff --git a/forge-game/src/main/java/forge/game/keyword/KeywordInstance.java b/forge-game/src/main/java/forge/game/keyword/KeywordInstance.java index 8964d458865..86d23aeebd4 100644 --- a/forge-game/src/main/java/forge/game/keyword/KeywordInstance.java +++ b/forge-game/src/main/java/forge/game/keyword/KeywordInstance.java @@ -146,7 +146,7 @@ public void createTraits(Player player, boolean clear) { } try { String msg = "KeywordInstance:createTraits: make Traits for Keyword"; - + Breadcrumb bread = new Breadcrumb(msg); bread.setData("Player", player.getName()); bread.setData("Keyword", this.original); @@ -212,6 +212,18 @@ public final void addStaticAbility(final StaticAbility st) { staticAbilities.add(st); } + public boolean hasTraits() { + if (!getAbilities().isEmpty()) + return true; + if (!getTriggers().isEmpty()) + return true; + if (!getReplacements().isEmpty()) + return true; + if (!getStaticAbilities().isEmpty()) + return true; + return false; + } + /* * (non-Javadoc) * @see forge.game.keyword.KeywordInterface#getTriggers() @@ -241,6 +253,24 @@ public Collection getStaticAbilities() { return staticAbilities; } + + public List applySpellAbility(List list) { + list.addAll(getAbilities()); + return list; + } + public List applyTrigger(List list) { + list.addAll(getTriggers()); + return list; + } + public List applyReplacementEffect(List list) { + list.addAll(getReplacements()); + return list; + } + public List applyStaticAbility(List list) { + list.addAll(getStaticAbilities()); + return list; + } + /* * (non-Javadoc) * @see forge.game.keyword.KeywordInterface#copy() @@ -393,7 +423,7 @@ public boolean hasSVar(final String name) { @Override public final void setSVar(final String name, final String value) { - + } @Override diff --git a/forge-game/src/main/java/forge/game/keyword/KeywordInterface.java b/forge-game/src/main/java/forge/game/keyword/KeywordInterface.java index 0b36f5705b8..38f37e4ac08 100644 --- a/forge-game/src/main/java/forge/game/keyword/KeywordInterface.java +++ b/forge-game/src/main/java/forge/game/keyword/KeywordInterface.java @@ -4,13 +4,14 @@ import forge.game.IHasSVars; import forge.game.card.Card; +import forge.game.card.ICardTraitChanges; import forge.game.player.Player; import forge.game.replacement.ReplacementEffect; import forge.game.spellability.SpellAbility; import forge.game.staticability.StaticAbility; import forge.game.trigger.Trigger; -public interface KeywordInterface extends Cloneable, IHasSVars { +public interface KeywordInterface extends Cloneable, IHasSVars, ICardTraitChanges { Card getHostCard(); void setHostCard(final Card host); @@ -38,6 +39,8 @@ public interface KeywordInterface extends Cloneable, IHasSVars { void createTraits(final Player player); void createTraits(final Player player, final boolean clear); + boolean hasTraits(); + void addTrigger(final Trigger trg); void addReplacement(final ReplacementEffect trg); diff --git a/forge-game/src/main/java/forge/game/keyword/KeywordsChange.java b/forge-game/src/main/java/forge/game/keyword/KeywordsChange.java index 102d8c29861..b72644c1b91 100644 --- a/forge-game/src/main/java/forge/game/keyword/KeywordsChange.java +++ b/forge-game/src/main/java/forge/game/keyword/KeywordsChange.java @@ -152,39 +152,34 @@ public KeywordsChange copy(final Card host, final boolean lki) { public List applySpellAbility(List list) { for (KeywordInterface k : this.keywords.getValues()) { - list.addAll(k.getAbilities()); + k.applySpellAbility(list); } return list; } public List applyTrigger(List list) { for (KeywordInterface k : this.keywords.getValues()) { - list.addAll(k.getTriggers()); + k.applyTrigger(list); } return list; } public List applyReplacementEffect(List list) { for (KeywordInterface k : this.keywords.getValues()) { - list.addAll(k.getReplacements()); + k.applyReplacementEffect(list); } return list; } public List applyStaticAbility(List list) { for (KeywordInterface k : this.keywords.getValues()) { - list.addAll(k.getStaticAbilities()); + k.applyStaticAbility(list); } return list; } public boolean hasTraits() { for (KeywordInterface k : this.keywords.getValues()) { - if (!k.getAbilities().isEmpty()) - return true; - if (!k.getTriggers().isEmpty()) - return true; - if (!k.getReplacements().isEmpty()) - return true; - if (!k.getStaticAbilities().isEmpty()) + if (k.hasTraits()) { return true; + } } return false; } From 9ba3a95a96673b0282a9f20c728483f28bbb9dfb Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Wed, 28 Jan 2026 07:29:32 +0100 Subject: [PATCH 2/3] use IKeywordsChange --- .../src/main/java/forge/game/card/Card.java | 12 ++++++------ .../main/java/forge/game/card/CardState.java | 10 ++++++++-- .../forge/game/keyword/IKeywordsChange.java | 8 ++++++++ .../forge/game/keyword/KeywordCollection.java | 17 +++-------------- .../java/forge/game/keyword/KeywordsChange.java | 17 ++++++++++++++++- .../src/main/java/forge/game/player/Player.java | 6 +++--- 6 files changed, 44 insertions(+), 26 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/keyword/IKeywordsChange.java 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 e6538f56852..1c2eea17484 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -126,7 +126,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr private final Table changedCardTypes = TreeBasedTable.create(); // Layer 4 private final Table changedCardNames = TreeBasedTable.create(); // Layer 3 - private final Table changedCardKeywordsByText = TreeBasedTable.create(); // Layer 3 by Text Change + private final Table changedCardKeywordsByText = TreeBasedTable.create(); // Layer 3 by Text Change protected KeywordsChange changedCardKeywordsByWord = new KeywordsChange(ImmutableList.of(), ImmutableList.of(), false); // Layer 3 by Word Change private final Table changedCardKeywords = TreeBasedTable.create(); // Layer 6 @@ -4130,15 +4130,15 @@ public boolean clearChangedCardColors() { return changed; } - public Table getChangedCardKeywordsByText() { + public Table getChangedCardKeywordsByText() { return changedCardKeywordsByText; } - public Iterable getChangedCardKeywordsList() { + public Iterable getChangedCardKeywordsList() { return Iterables.concat( changedCardKeywordsByText.values(), // Layer 3 ImmutableList.of(changedCardKeywordsByWord), // Layer 3 - ImmutableList.of(new KeywordsChange(ImmutableList.of(), ImmutableList.of(), this.hasRemoveIntrinsic())), // Layer 4 + ImmutableList.of(getCurrentState().getLandTraitChanges()), // Layer 4 changedCardKeywords.values() // Layer 6 ); } @@ -5146,9 +5146,9 @@ public final void addChangedCardKeywordsByText(final List keyw } } - public void setChangedCardKeywordsByText(Table changedCardKeywords) { + public void setChangedCardKeywordsByText(Table changedCardKeywords) { this.changedCardKeywordsByText.clear(); - for (Table.Cell entry : changedCardKeywords.cellSet()) { + for (Table.Cell entry : changedCardKeywords.cellSet()) { this.changedCardKeywordsByText.put(entry.getRowKey(), entry.getColumnKey(), entry.getValue().copy(this, true)); } } 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 a6c27171e2f..bf859ece175 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -29,6 +29,7 @@ import forge.game.ability.AbilityFactory; import forge.game.ability.ApiType; import forge.game.card.CardView.CardStateView; +import forge.game.keyword.IKeywordsChange; import forge.game.keyword.Keyword; import forge.game.keyword.KeywordCollection; import forge.game.keyword.KeywordInterface; @@ -483,7 +484,7 @@ protected final void updateSpellAbilities(FCollection newCol) { public LandTraitChanges getLandTraitChanges() { return this.landTraitChanges; } - record LandTraitChanges(CardState state, Map map) implements ICardTraitChanges + record LandTraitChanges(CardState state, Map map) implements ICardTraitChanges, IKeywordsChange { LandTraitChanges(CardState state) { this(state, Maps.newEnumMap(MagicColor.Color.class)); @@ -531,7 +532,12 @@ public List applyStaticAbility(List list) { } return list; } - public ICardTraitChanges copy(Card host, boolean lki) { return this; } + public void applyKeywords(KeywordCollection list) { + if (state.getCard().hasRemoveIntrinsic()) { + list.clear(); + } + } + public LandTraitChanges copy(Card host, boolean lki) { return this; } } public final Iterable getIntrinsicSpellAbilities() { diff --git a/forge-game/src/main/java/forge/game/keyword/IKeywordsChange.java b/forge-game/src/main/java/forge/game/keyword/IKeywordsChange.java new file mode 100644 index 00000000000..fdb4e42105b --- /dev/null +++ b/forge-game/src/main/java/forge/game/keyword/IKeywordsChange.java @@ -0,0 +1,8 @@ +package forge.game.keyword; + +import forge.game.card.Card; + +public interface IKeywordsChange { + void applyKeywords(KeywordCollection list); + public IKeywordsChange copy(final Card host, final boolean lki); +} diff --git a/forge-game/src/main/java/forge/game/keyword/KeywordCollection.java b/forge-game/src/main/java/forge/game/keyword/KeywordCollection.java index 63202efbdca..135af59d327 100644 --- a/forge-game/src/main/java/forge/game/keyword/KeywordCollection.java +++ b/forge-game/src/main/java/forge/game/keyword/KeywordCollection.java @@ -180,20 +180,9 @@ public KeywordCollectionView getView() { return view; } - public void applyChanges(Iterable changes) { - for (final KeywordsChange ck : changes) { - if (ck.isRemoveAllKeywords()) { - clear(); - } - else if (ck.getRemoveKeywords() != null) { - removeAll(ck.getRemoveKeywords()); - } - - removeInstances(ck.getRemovedKeywordInstances()); - - if (ck.getKeywords() != null) { - insertAll(ck.getKeywords()); - } + public void applyChanges(Iterable changes) { + for (final IKeywordsChange ck : changes) { + ck.applyKeywords(this); } } diff --git a/forge-game/src/main/java/forge/game/keyword/KeywordsChange.java b/forge-game/src/main/java/forge/game/keyword/KeywordsChange.java index b72644c1b91..1a95d2dd5fc 100644 --- a/forge-game/src/main/java/forge/game/keyword/KeywordsChange.java +++ b/forge-game/src/main/java/forge/game/keyword/KeywordsChange.java @@ -36,7 +36,7 @@ * * @author Forge */ -public class KeywordsChange implements ICardTraitChanges, Cloneable { +public class KeywordsChange implements ICardTraitChanges, IKeywordsChange, Cloneable { private KeywordCollection keywords = new KeywordCollection(); private List removeKeywordInterfaces = Lists.newArrayList(); private List removeKeywords = Lists.newArrayList(); @@ -175,6 +175,21 @@ public List applyStaticAbility(List list) { return list; } + public void applyKeywords(KeywordCollection list) { + if (isRemoveAllKeywords()) { + list.clear(); + } + else if (getRemoveKeywords() != null) { + list.removeAll(getRemoveKeywords()); + } + + list.removeInstances(getRemovedKeywordInstances()); + + if (getKeywords() != null) { + list.insertAll(getKeywords()); + } + } + public boolean hasTraits() { for (KeywordInterface k : this.keywords.getValues()) { if (k.hasTraits()) { diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index d1d83371f36..9d93fa4c2ed 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -143,7 +143,7 @@ public class Player extends GameEntity implements Comparable { private KeywordCollection keywords = new KeywordCollection(); // stores the keywords created by static abilities private final Table storedKeywords = TreeBasedTable.create(); - private Table changedKeywords = TreeBasedTable.create(); + private Table changedKeywords = TreeBasedTable.create(); private Map> attackedThisTurn = new HashMap<>(); private List attackedPlayersLastTurn = new ArrayList<>(); @@ -1008,8 +1008,8 @@ public final KeywordInterface getKeywordForStaticAbility(String kw, final long s return result; } - public final KeywordsChange removeChangedKeywords(final Long timestamp, final long staticId) { - KeywordsChange change = changedKeywords.remove(timestamp, staticId); + public final IKeywordsChange removeChangedKeywords(final Long timestamp, final long staticId) { + IKeywordsChange change = changedKeywords.remove(timestamp, staticId); if (change != null) { if (keywordEffect != null) { getKeywordCard().removeChangedCardTraits(timestamp, staticId); From 45e21cb6f4495043e88d6ce4ec168e537c0c64ec Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Wed, 28 Jan 2026 09:58:17 +0100 Subject: [PATCH 3/3] Use CardState for getChangedCardKeywordsList --- forge-game/src/main/java/forge/game/card/Card.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 1c2eea17484..4b3f83ed0fb 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -4134,11 +4134,11 @@ public Table getChangedCardKeywordsByText() { return changedCardKeywordsByText; } - public Iterable getChangedCardKeywordsList() { + public Iterable getChangedCardKeywordsList(final CardState state) { return Iterables.concat( changedCardKeywordsByText.values(), // Layer 3 ImmutableList.of(changedCardKeywordsByWord), // Layer 3 - ImmutableList.of(getCurrentState().getLandTraitChanges()), // Layer 4 + ImmutableList.of(state.getLandTraitChanges()), // Layer 4 changedCardKeywords.values() // Layer 6 ); } @@ -5236,7 +5236,7 @@ public final void updateKeywordsCache(final CardState state) { } } - keywords.applyChanges(getChangedCardKeywordsList()); + keywords.applyChanges(getChangedCardKeywordsList(state)); // remove Can't have keywords for (Keyword k : getCantHaveKeyword()) {