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 <kevni@secure.mailbox.org>
Co-authored-by: tool4EvEr <tool4EvEr@>
This commit is contained in:
kvn1338
2025-06-01 09:19:50 +02:00
committed by GitHub
parent 928ac875b5
commit e40567c9c8
10 changed files with 190 additions and 8 deletions

View File

@@ -92,6 +92,7 @@ public enum ApiType {
ExchangeControlVariant (ControlExchangeVariantEffect.class), ExchangeControlVariant (ControlExchangeVariantEffect.class),
ExchangePower (PowerExchangeEffect.class), ExchangePower (PowerExchangeEffect.class),
ExchangeZone (ZoneExchangeEffect.class), ExchangeZone (ZoneExchangeEffect.class),
ExchangeTextBox (TextBoxExchangeEffect.class),
Explore (ExploreEffect.class), Explore (ExploreEffect.class),
Fight (FightEffect.class), Fight (FightEffect.class),
FlipACoin (FlipCoinEffect.class), FlipACoin (FlipCoinEffect.class),

View File

@@ -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<Card> 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<Card> 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<SpellAbility> 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<Trigger> 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<ReplacementEffect> 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<StaticAbility> 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<KeywordInterface> 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<SpellAbility> spellabilities;
List<Trigger> triggers;
List<ReplacementEffect> replacements;
List<StaticAbility> statics;
List<KeywordInterface> keywords;
}
}

View File

@@ -5324,6 +5324,13 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
} }
} }
public void setChangedCardKeywordsByText(Table<Long, Long, KeywordsChange> changedCardKeywords) {
this.changedCardKeywordsByText.clear();
for (Table.Cell<Long, Long, KeywordsChange> entry : changedCardKeywords.cellSet()) {
this.changedCardKeywordsByText.put(entry.getRowKey(), entry.getColumnKey(), entry.getValue().copy(this, true));
}
}
public final void addChangedCardKeywordsInternal( public final void addChangedCardKeywordsInternal(
final Collection<KeywordInterface> keywords, final Collection<KeywordInterface> removeKeywords, final Collection<KeywordInterface> keywords, final Collection<KeywordInterface> removeKeywords,
final boolean removeAllKeywords, final boolean removeAllKeywords,
@@ -8351,5 +8358,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
this.changedCardNames.putAll(in.changedCardNames); this.changedCardNames.putAll(in.changedCardNames);
setChangedCardTraits(in.getChangedCardTraits()); setChangedCardTraits(in.getChangedCardTraits());
setChangedCardTraitsByText(in.getChangedCardTraitsByText());
setChangedCardKeywordsByText(in.getChangedCardKeywordsByText());
} }
} }

View File

@@ -360,7 +360,8 @@ public class CardCopyService {
newCopy.setStoredReplacements(copyFrom.getStoredReplacements()); newCopy.setStoredReplacements(copyFrom.getStoredReplacements());
newCopy.copyChangedTextFrom(copyFrom); newCopy.copyChangedTextFrom(copyFrom);
newCopy.updateChangedText(); newCopy.changedTypeByText = copyFrom.changedTypeByText;
newCopy.changedCardKeywordsByWord = copyFrom.changedCardKeywordsByWord.copy(newCopy, true);
newCopy.setGameTimestamp(copyFrom.getGameTimestamp()); newCopy.setGameTimestamp(copyFrom.getGameTimestamp());
newCopy.setLayerTimestamp(copyFrom.getLayerTimestamp()); newCopy.setLayerTimestamp(copyFrom.getLayerTimestamp());

View File

@@ -181,15 +181,18 @@ public abstract class ReplacementEffect extends TriggerReplacementBase {
return meetsCommonRequirements(getMapParams()); return meetsCommonRequirements(getMapParams());
} }
public final ReplacementEffect copy(Card newHost, boolean lki) {
return copy(newHost, lki, false);
}
/** /**
* Gets the copy. * Gets the copy.
* *
* @return 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(); final ReplacementEffect res = (ReplacementEffect) clone();
copyHelper(res, host); copyHelper(res, host, lki || keepTextChanges);
final SpellAbility sa = this.getOverridingAbility(); final SpellAbility sa = this.getOverridingAbility();
if (sa != null) { if (sa != null) {

View File

@@ -1208,6 +1208,9 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
public SpellAbility copy(Card host, final boolean lki) { public SpellAbility copy(Card host, final boolean lki) {
return copy(host, this.getActivatingPlayer(), 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) { public SpellAbility copy(Card host, Player activ, final boolean lki) {
return copy(host, activ, lki, false); return copy(host, activ, lki, false);
} }

View File

@@ -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; StaticAbility clone = null;
try { try {
clone = (StaticAbility) clone(); clone = (StaticAbility) clone();
clone.id = lki ? id : nextId(); clone.id = lki ? id : nextId();
copyHelper(clone, host); copyHelper(clone, host, lki || keepTextChanges);
// reset to force refresh if needed // reset to force refresh if needed
clone.payingTrigSA = null; clone.payingTrigSA = null;

View File

@@ -532,9 +532,12 @@ public abstract class Trigger extends TriggerReplacementBase {
} }
public final Trigger copy(Card newHost, boolean lki) { 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(); final Trigger copy = (Trigger) clone();
copyHelper(copy, newHost); copyHelper(copy, newHost, lki || keepTextChanges);
if (getOverridingAbility() != null) { if (getOverridingAbility() != null) {
copy.setOverridingAbility(getOverridingAbility().copy(newHost, lki)); copy.setOverridingAbility(getOverridingAbility().copy(newHost, lki));

View File

@@ -1,2 +1,2 @@
- Bug fixes - - 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. As always, this release of Forge features an assortment of bug fixes and improvements based on user feedback during the previous release run.

View File

@@ -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.