From e40567c9c8ccddb539112542991c8f46dc85d77d Mon Sep 17 00:00:00 2001 From: kvn1338 Date: Sun, 1 Jun 2025 09:19:50 +0200 Subject: [PATCH] Add TextBoxExchangeEffect Ability and 'Deadpool, Trading Card' (#7637) * capure Textboxes to avoid LKI copy * Fix copying keyworded traits twice * Support keepTextChanges across all traits Co-authored-by: kvn Co-authored-by: tool4EvEr --- .../main/java/forge/game/ability/ApiType.java | 1 + .../effects/TextBoxExchangeEffect.java | 146 ++++++++++++++++++ .../src/main/java/forge/game/card/Card.java | 10 ++ .../java/forge/game/card/CardCopyService.java | 3 +- .../game/replacement/ReplacementEffect.java | 7 +- .../forge/game/spellability/SpellAbility.java | 3 + .../game/staticability/StaticAbility.java | 7 +- .../main/java/forge/game/trigger/Trigger.java | 5 +- forge-gui/release-files/CHANGES.txt | 4 +- .../cardsfolder/d/deadpool_trading_card.txt | 12 ++ 10 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/ability/effects/TextBoxExchangeEffect.java create mode 100644 forge-gui/res/cardsfolder/d/deadpool_trading_card.txt diff --git a/forge-game/src/main/java/forge/game/ability/ApiType.java b/forge-game/src/main/java/forge/game/ability/ApiType.java index 54aab038fee..db71881a254 100644 --- a/forge-game/src/main/java/forge/game/ability/ApiType.java +++ b/forge-game/src/main/java/forge/game/ability/ApiType.java @@ -92,6 +92,7 @@ public enum ApiType { ExchangeControlVariant (ControlExchangeVariantEffect.class), ExchangePower (PowerExchangeEffect.class), ExchangeZone (ZoneExchangeEffect.class), + ExchangeTextBox (TextBoxExchangeEffect.class), Explore (ExploreEffect.class), Fight (FightEffect.class), FlipACoin (FlipCoinEffect.class), diff --git a/forge-game/src/main/java/forge/game/ability/effects/TextBoxExchangeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/TextBoxExchangeEffect.java new file mode 100644 index 00000000000..654935d1389 --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/TextBoxExchangeEffect.java @@ -0,0 +1,146 @@ +package forge.game.ability.effects; + +import com.google.common.collect.Lists; + +import forge.game.Game; +import forge.game.ability.SpellAbilityEffect; +import forge.game.card.Card; +import forge.game.card.CardState; +import forge.game.event.GameEventCardStatsChanged; +import forge.game.keyword.KeywordInterface; +import forge.game.replacement.ReplacementEffect; +import forge.game.spellability.SpellAbility; +import forge.game.staticability.StaticAbility; +import forge.game.trigger.Trigger; + +import java.util.List; + +/** + * Exchanges text boxes between two creatures. + */ +public class TextBoxExchangeEffect extends SpellAbilityEffect { + @Override + protected String getStackDescription(final SpellAbility sa) { + final List tgtCards = getTargetCards(sa); + Card c1; + Card c2; + if (tgtCards.size() == 1) { + c1 = sa.getHostCard(); + c2 = tgtCards.get(0); + } else { + c1 = tgtCards.get(0); + c2 = tgtCards.get(1); + } + return c1 + " exchanges text box with " + c2 + "."; + } + + @Override + public void resolve(final SpellAbility sa) { + final List tgtCards = getTargetCards(sa); + if (tgtCards.size() < 2) { + return; + } + + final Card c1 = tgtCards.get(0); + final Card c2 = tgtCards.get(1); + + // snapshot the original text boxes before modifying + final TextBoxData data1 = captureTextBoxData(c1); + final TextBoxData data2 = captureTextBoxData(c2); + + final Card host = sa.getHostCard(); + final Game game = host.getGame(); + final long ts = game.getNextTimestamp(); + + swapTextBox(c1, data2, ts); + swapTextBox(c2, data1, ts); + + game.fireEvent(new GameEventCardStatsChanged(c1)); + game.fireEvent(new GameEventCardStatsChanged(c2)); + } + + private static void swapTextBox(final Card to, final TextBoxData from, final long ts) { + List spellabilities = Lists.newArrayList(); + for (SpellAbility sa : from.spellabilities) { + SpellAbility copy = sa.copy(to, false, true); + // need to persist any previous word changes + copy.changeTextIntrinsic(copy.getChangedTextColors(), copy.getChangedTextTypes()); + spellabilities.add(copy); + } + List triggers = Lists.newArrayList(); + for (Trigger tr : from.triggers) { + Trigger copy = tr.copy(to, false, true); + copy.changeTextIntrinsic(copy.getChangedTextColors(), copy.getChangedTextTypes()); + triggers.add(copy); + } + List reps = Lists.newArrayList(); + for (ReplacementEffect re : from.replacements) { + ReplacementEffect copy = re.copy(to, false, true); + copy.changeTextIntrinsic(copy.getChangedTextColors(), copy.getChangedTextTypes()); + reps.add(copy); + } + List statics = Lists.newArrayList(); + for (StaticAbility st : from.statics) { + StaticAbility copy = st.copy(to, false, true); + copy.changeTextIntrinsic(copy.getChangedTextColors(), copy.getChangedTextTypes()); + statics.add(copy); + } + to.addChangedCardTraitsByText(spellabilities, triggers, reps, statics, ts, 0); + + List kws = Lists.newArrayList(); + for (KeywordInterface kw : from.keywords) { + kws.add(kw.copy(to, false)); + } + to.addChangedCardKeywordsByText(kws, ts, 0, false); + + to.updateChangedText(); + to.updateStateForView(); + } + + private static TextBoxData captureTextBoxData(final Card card) { + TextBoxData data = new TextBoxData(); + CardState state = card.getCurrentState(); + + data.spellabilities = Lists.newArrayList(); + for (SpellAbility sa : state.getSpellAbilities()) { + if (sa.isIntrinsic() && sa.getKeyword() == null) { + data.spellabilities.add(sa); + } + } + data.triggers = Lists.newArrayList(); + for (Trigger tr : state.getTriggers()) { + if (tr.isIntrinsic() && tr.getKeyword() == null) { + data.triggers.add(tr); + } + } + data.replacements = Lists.newArrayList(); + for (ReplacementEffect re : state.getReplacementEffects()) { + if (re.isIntrinsic() && re.getKeyword() == null) { + data.replacements.add(re); + } + } + data.statics = Lists.newArrayList(); + for (StaticAbility st : state.getStaticAbilities()) { + if (st.isIntrinsic() && st.getKeyword() == null) { + data.statics.add(st); + } + } + + data.keywords = Lists.newArrayList(); + for (KeywordInterface ki : card.getKeywords()) { + if (ki.isIntrinsic()) { + data.keywords.add(ki); + } + } + + return data; + } + + private static class TextBoxData { + List spellabilities; + List triggers; + List replacements; + List statics; + List keywords; + } +} 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 85c81155fc5..ba19960d605 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -5324,6 +5324,13 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr } } + public void setChangedCardKeywordsByText(Table changedCardKeywords) { + this.changedCardKeywordsByText.clear(); + for (Table.Cell entry : changedCardKeywords.cellSet()) { + this.changedCardKeywordsByText.put(entry.getRowKey(), entry.getColumnKey(), entry.getValue().copy(this, true)); + } + } + public final void addChangedCardKeywordsInternal( final Collection keywords, final Collection removeKeywords, final boolean removeAllKeywords, @@ -8351,5 +8358,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr this.changedCardNames.putAll(in.changedCardNames); setChangedCardTraits(in.getChangedCardTraits()); + + setChangedCardTraitsByText(in.getChangedCardTraitsByText()); + setChangedCardKeywordsByText(in.getChangedCardKeywordsByText()); } } diff --git a/forge-game/src/main/java/forge/game/card/CardCopyService.java b/forge-game/src/main/java/forge/game/card/CardCopyService.java index 0fbcd834c13..bfe464d1d9e 100644 --- a/forge-game/src/main/java/forge/game/card/CardCopyService.java +++ b/forge-game/src/main/java/forge/game/card/CardCopyService.java @@ -360,7 +360,8 @@ public class CardCopyService { newCopy.setStoredReplacements(copyFrom.getStoredReplacements()); newCopy.copyChangedTextFrom(copyFrom); - newCopy.updateChangedText(); + newCopy.changedTypeByText = copyFrom.changedTypeByText; + newCopy.changedCardKeywordsByWord = copyFrom.changedCardKeywordsByWord.copy(newCopy, true); newCopy.setGameTimestamp(copyFrom.getGameTimestamp()); newCopy.setLayerTimestamp(copyFrom.getLayerTimestamp()); diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java b/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java index e56e8387831..f81dbb8f47d 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java @@ -181,15 +181,18 @@ public abstract class ReplacementEffect extends TriggerReplacementBase { return meetsCommonRequirements(getMapParams()); } + public final ReplacementEffect copy(Card newHost, boolean lki) { + return copy(newHost, lki, false); + } /** * Gets the copy. * * @return the copy */ - public final ReplacementEffect copy(final Card host, final boolean lki) { + public final ReplacementEffect copy(final Card host, final boolean lki, boolean keepTextChanges) { final ReplacementEffect res = (ReplacementEffect) clone(); - copyHelper(res, host); + copyHelper(res, host, lki || keepTextChanges); final SpellAbility sa = this.getOverridingAbility(); if (sa != null) { diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 216faed01da..062e5e9a739 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -1208,6 +1208,9 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public SpellAbility copy(Card host, final boolean lki) { return copy(host, this.getActivatingPlayer(), lki); } + public SpellAbility copy(Card host, final boolean lki, boolean keepTextChanges) { + return copy(host, this.getActivatingPlayer(), lki, keepTextChanges); + } public SpellAbility copy(Card host, Player activ, final boolean lki) { return copy(host, activ, lki, false); } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java index d96220a10bf..957f10a494f 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java @@ -591,13 +591,16 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone } } - public StaticAbility copy(Card host, final boolean lki) { + public final StaticAbility copy(Card newHost, boolean lki) { + return copy(newHost, lki, false); + } + public StaticAbility copy(Card host, final boolean lki, boolean keepTextChanges) { StaticAbility clone = null; try { clone = (StaticAbility) clone(); clone.id = lki ? id : nextId(); - copyHelper(clone, host); + copyHelper(clone, host, lki || keepTextChanges); // reset to force refresh if needed clone.payingTrigSA = null; diff --git a/forge-game/src/main/java/forge/game/trigger/Trigger.java b/forge-game/src/main/java/forge/game/trigger/Trigger.java index 409ac3d7a94..dbcd5ee48de 100644 --- a/forge-game/src/main/java/forge/game/trigger/Trigger.java +++ b/forge-game/src/main/java/forge/game/trigger/Trigger.java @@ -532,9 +532,12 @@ public abstract class Trigger extends TriggerReplacementBase { } public final Trigger copy(Card newHost, boolean lki) { + return copy(newHost, lki, false); + } + public final Trigger copy(Card newHost, boolean lki, boolean keepTextChanges) { final Trigger copy = (Trigger) clone(); - copyHelper(copy, newHost); + copyHelper(copy, newHost, lki || keepTextChanges); if (getOverridingAbility() != null) { copy.setOverridingAbility(getOverridingAbility().copy(newHost, lki)); diff --git a/forge-gui/release-files/CHANGES.txt b/forge-gui/release-files/CHANGES.txt index 00d9ec2e98d..05f0bf1829e 100644 --- a/forge-gui/release-files/CHANGES.txt +++ b/forge-gui/release-files/CHANGES.txt @@ -1,2 +1,2 @@ -- Bug fixes - -As always, this release of Forge features an assortment of bug fixes and improvements based on user feedback during the previous release run. +- Bug fixes - +As always, this release of Forge features an assortment of bug fixes and improvements based on user feedback during the previous release run. diff --git a/forge-gui/res/cardsfolder/d/deadpool_trading_card.txt b/forge-gui/res/cardsfolder/d/deadpool_trading_card.txt new file mode 100644 index 00000000000..4195003001c --- /dev/null +++ b/forge-gui/res/cardsfolder/d/deadpool_trading_card.txt @@ -0,0 +1,12 @@ +Name:Deadpool, Trading Card +ManaCost:2 B R +Types:Legendary Creature Mutant Mercenary Hero +PT:5/3 +K:ETBReplacement:Other:DBChooseExchange:Optional +SVar:DBChooseExchange:DB$ ChooseCard | Defined$ You | Choices$ Creature.Other | ChoiceTitle$ Choose a creature to exchange text boxes with | SubAbility$ DBExchangeText | SpellDescription$ As NICKNAME enters, you may exchange his text box and another creature's. +SVar:DBExchangeText:DB$ ExchangeTextBox | Defined$ Self & ChosenCard +T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ Lose3 | TriggerDescription$ At the beginning of your upkeep, you lose 3 life. +SVar:Lose3:DB$ LoseLife | Defined$ TriggeredPlayer | LifeAmount$ 3 +A:AB$ Draw | Cost$ 3 Sac<1/CARDNAME> | Defined$ Player.Other | NumCards$ 1 | SpellDescription$ Each other player draws a card. +AI:RemoveDeck:All +Oracle:As Deadpool enters, you may exchange his text box and another creature's.\nAt the beginning of your upkeep, you lose 3 life.\n{3}, Sacrifice this creature: Each other player draws a card.