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),
ExchangePower (PowerExchangeEffect.class),
ExchangeZone (ZoneExchangeEffect.class),
ExchangeTextBox (TextBoxExchangeEffect.class),
Explore (ExploreEffect.class),
Fight (FightEffect.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(
final Collection<KeywordInterface> keywords, final Collection<KeywordInterface> removeKeywords,
final boolean removeAllKeywords,
@@ -8351,5 +8358,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
this.changedCardNames.putAll(in.changedCardNames);
setChangedCardTraits(in.getChangedCardTraits());
setChangedCardTraitsByText(in.getChangedCardTraitsByText());
setChangedCardKeywordsByText(in.getChangedCardKeywordsByText());
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

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.