From bce5466c625e27324b6b754d8b2c8fb25608bec0 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Tue, 11 Nov 2025 08:46:53 +0100 Subject: [PATCH] Trigger: reuse Execute for multiple Trigger (#9094) * Trigger: reuse Execute for multiple Trigger * Update CardState.java copy abilityForTrigger * Update CardState.java * Update Trigger.java * Update CardState.java * Fix Multi Trigger with Idris * Fix Oasis of Renewal ActivationLimit * fix storedAbilityForTrigger * remove hasAbilityForTrigger * Update script --------- Co-authored-by: tool4EvEr --- .../src/main/java/forge/game/card/Card.java | 11 +++++++++- .../main/java/forge/game/card/CardState.java | 12 ++++++++++- .../main/java/forge/game/trigger/Trigger.java | 15 +++++++++++--- .../cardsfolder/c/corruption_of_towashi.txt | 10 +++------- .../res/cardsfolder/o/oasis_of_renewal.txt | 20 ++++++------------- .../v/victor_valgavoths_seneschal.txt | 11 ++++------ 6 files changed, 46 insertions(+), 33 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 d3f34d699f6..da2e2c153a4 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -140,6 +140,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr // stores the card traits created by static abilities private final Table storedSpellAbility = TreeBasedTable.create(); private final Table storedTrigger = TreeBasedTable.create(); + private final Table storedAbilityForTrigger = HashBasedTable.create(); private final Table storedReplacementEffect = TreeBasedTable.create(); private final Table storedStaticAbility = TreeBasedTable.create(); @@ -4974,7 +4975,15 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr String str = trig.toString() + trig.getId(); Trigger result = storedTrigger.get(stAb, str); if (result == null) { - result = trig.copy(this, false); + SpellAbility ab = null; + if (trig.hasParam("Execute") && trig.getOverridingAbility() != null) { + ab = storedAbilityForTrigger.get(stAb, trig.getOverridingAbility()); + if (ab == null) { + ab = trig.getOverridingAbility().copy(this, false); + storedAbilityForTrigger.put(stAb, trig.getOverridingAbility(), ab); + } + } + result = trig.copy(this, false, false, ab); storedTrigger.put(stAb, str, result); } return result; 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 549421395e6..82774a61535 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -79,6 +79,7 @@ public class CardState implements GameObject, IHasSVars, ITranslatable { private FCollection staticAbilities = new FCollection<>(); private String imageKey = ""; private Map sVars = Maps.newTreeMap(); + private Map abilityForTrigger = Maps.newHashMap(); private KeywordCollection cachedKeywords = new KeywordCollection(); @@ -732,6 +733,11 @@ public class CardState implements GameObject, IHasSVars, ITranslatable { setFlavorName(source.getFlavorName()); setSVars(source.getSVars()); + abilityForTrigger.clear(); + for (Map.Entry e : source.abilityForTrigger.entrySet()) { + abilityForTrigger.put(e.getKey(), e.getValue().copy(card, lki)); + } + abilities.clear(); for (SpellAbility sa : source.abilities) { if (sa.isIntrinsic()) { @@ -758,7 +764,7 @@ public class CardState implements GameObject, IHasSVars, ITranslatable { continue; } if (tr.isIntrinsic()) { - triggers.add(tr.copy(card, lki)); + triggers.add(tr.copy(card, lki, false, tr.hasParam("Execute") ? abilityForTrigger.get(tr.getParam("Execute")) : null)); } } ReplacementEffect runRE = null; @@ -951,6 +957,10 @@ public class CardState implements GameObject, IHasSVars, ITranslatable { return cloakUp; } + public SpellAbility getAbilityForTrigger(String svar) { + return abilityForTrigger.computeIfAbsent(svar, s -> AbilityFactory.getAbility(getCard(), s, this)); + } + @Override public String getTranslationKey() { String displayName = flavorName == null ? name : flavorName; 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 6ff534df4c9..12403d2541c 100644 --- a/forge-game/src/main/java/forge/game/trigger/Trigger.java +++ b/forge-game/src/main/java/forge/game/trigger/Trigger.java @@ -544,14 +544,19 @@ public abstract class Trigger extends TriggerReplacementBase { } public final Trigger copy(Card newHost, boolean lki) { - return copy(newHost, lki, false); + return copy(newHost, lki, false, null); } public final Trigger copy(Card newHost, boolean lki, boolean keepTextChanges) { + return copy(newHost, lki, keepTextChanges, null); + } + public final Trigger copy(Card newHost, boolean lki, boolean keepTextChanges, SpellAbility spellAbility) { final Trigger copy = (Trigger) clone(); copyHelper(copy, newHost, lki || keepTextChanges); - if (getOverridingAbility() != null) { + if (spellAbility != null) { + copy.setOverridingAbility(spellAbility); + } else if (getOverridingAbility() != null) { copy.setOverridingAbility(getOverridingAbility().copy(newHost, lki)); } @@ -611,7 +616,11 @@ public abstract class Trigger extends TriggerReplacementBase { public SpellAbility ensureAbility(final IHasSVars sVarHolder) { SpellAbility sa = getOverridingAbility(); if (sa == null && hasParam("Execute")) { - sa = AbilityFactory.getAbility(getHostCard(), getParam("Execute"), sVarHolder); + if (this.isIntrinsic() && sVarHolder instanceof CardState state) { + sa = state.getAbilityForTrigger(getParam("Execute")); + } else { + sa = AbilityFactory.getAbility(getHostCard(), getParam("Execute"), sVarHolder); + } setOverridingAbility(sa); } return sa; diff --git a/forge-gui/res/cardsfolder/c/corruption_of_towashi.txt b/forge-gui/res/cardsfolder/c/corruption_of_towashi.txt index 9fc295c3ed2..55aaa0b721e 100644 --- a/forge-gui/res/cardsfolder/c/corruption_of_towashi.txt +++ b/forge-gui/res/cardsfolder/c/corruption_of_towashi.txt @@ -3,12 +3,8 @@ ManaCost:4 U Types:Enchantment T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigIncubate | TriggerDescription$ When CARDNAME enters, incubate 4. (Create an Incubator token with four +1/+1 counters on it and "{2}: Transform this artifact." It transforms into a 0/0 Phyrexian artifact creature.) SVar:TrigIncubate:DB$ Incubate | Amount$ 4 -T:Mode$ Transformed | ValidCard$ Permanent.YouCtrl+inZoneBattlefield | Execute$ TrigDraw | TriggerZones$ Battlefield | OptionalDecider$ You | CheckSVar$ X | SVarCompare$ LT1 | TriggerDescription$ Whenever a permanent you control transforms or a permanent you control enters transformed, you may draw a card. Do this only once each turn. -T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Permanent.Transformed+YouCtrl | OptionalDecider$ You | TriggerZones$ Battlefield | Execute$ TrigDraw | Secondary$ True | CheckSVar$ X | SVarCompare$ LT1 | TriggerDescription$ Whenever a permanent you control transforms or a permanent you control enters transformed, you may draw a card. Do this only once each turn. -SVar:TrigDraw:DB$ Draw | SubAbility$ DBLog -SVar:DBLog:DB$ StoreSVar | SVar$ X | Type$ Number | Expression$ 1 -SVar:X:Number$0 -T:Mode$ Phase | Phase$ Cleanup | TriggerZones$ Battlefield | Execute$ DBCleanup | Static$ True -SVar:DBCleanup:DB$ StoreSVar | SVar$ X | Type$ Number | Expression$ 0 +T:Mode$ Transformed | ValidCard$ Permanent.YouCtrl+inZoneBattlefield | Execute$ TrigDraw | TriggerZones$ Battlefield | OptionalDecider$ You | ResolvedLimit$ 1 | TriggerDescription$ Whenever a permanent you control transforms or a permanent you control enters transformed, you may draw a card. Do this only once each turn. +T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Permanent.Transformed+YouCtrl | OptionalDecider$ You | TriggerZones$ Battlefield | Execute$ TrigDraw | Secondary$ True | ResolvedLimit$ 1 | TriggerDescription$ Whenever a permanent you control transforms or a permanent you control enters transformed, you may draw a card. Do this only once each turn. +SVar:TrigDraw:DB$ Draw DeckHas:Ability$Counters|Token & Type$Incubator|Artifact|Phyrexian Oracle:When Corruption of Towashi enters, incubate 4. (Create an Incubator token with four +1/+1 counters on it and "{2}: Transform this artifact." It transforms into a 0/0 Phyrexian artifact creature.)\nWhenever a permanent you control transforms or a permanent you control enters transformed, you may draw a card. Do this only once each turn. diff --git a/forge-gui/res/cardsfolder/o/oasis_of_renewal.txt b/forge-gui/res/cardsfolder/o/oasis_of_renewal.txt index d6ffb1bacc5..2258c241276 100644 --- a/forge-gui/res/cardsfolder/o/oasis_of_renewal.txt +++ b/forge-gui/res/cardsfolder/o/oasis_of_renewal.txt @@ -1,18 +1,10 @@ Name:Oasis of Renewal ManaCost:B G U Types:Legendary Enchantment -T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigSeekLand | TriggerDescription$ When CARDNAME enters and whenever a land card leaves your graveyard, seek a land card. This ability triggers only once each turn. -T:Mode$ ChangesZone | Origin$ Graveyard | Destination$ Any | ValidCard$ Card.Land+YouOwn | TriggerZones$ Battlefield | Execute$ TrigSeekLand | Secondary$ True | CheckSVar$ X | SVarCompare$ LT1 | TriggerDescription$ When CARDNAME enters and whenever a land card leaves your graveyard, seek a land card. This ability triggers only once each turn. -SVar:TrigSeekLand:DB$ Seek | Type$ Card.Land | SubAbility$ DBLogLand -SVar:DBLogLand:DB$ StoreSVar | SVar$ X | Type$ Number | Expression$ 1 -SVar:X:Number$0 -T:Mode$ Phase | Phase$ Cleanup | TriggerZones$ Battlefield | Execute$ DBLandClean | Static$ True -SVar:DBLandClean:DB$ StoreSVar | SVar$ X | Type$ Number | Expression$ 0 -T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigSeekNonLand | TriggerDescription$ When CARDNAME enters and whenever a nonland card leaves your graveyard, seek a nonland card. This ability triggers only once each turn. -T:Mode$ ChangesZone | Origin$ Graveyard | Destination$ Any | ValidCard$ Card.nonLand+YouOwn | TriggerZones$ Battlefield | Execute$ TrigSeekNonLand | Secondary$ True | CheckSVar$ Y | SVarCompare$ LT1 | TriggerDescription$ When CARDNAME enters and whenever a nonland card leaves your graveyard, seek a nonland card. This ability triggers only once each turn. -SVar:TrigSeekNonLand:DB$ Seek | Type$ Card.nonLand | SubAbility$ DBLogNonLand -SVar:DBLogNonLand:DB$ StoreSVar | SVar$ Y | Type$ Number | Expression$ 1 -SVar:Y:Number$0 -T:Mode$ Phase | Phase$ Cleanup | TriggerZones$ Battlefield | Execute$ DBNonLandClean | Static$ True -SVar:DBNonLandClean:DB$ StoreSVar | SVar$ Y | Type$ Number | Expression$ 0 +T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | ActivationLimit$ 1 | Execute$ TrigSeekLand | TriggerDescription$ When CARDNAME enters and whenever a land card leaves your graveyard, seek a land card. This ability triggers only once each turn. +T:Mode$ ChangesZone | Origin$ Graveyard | Destination$ Any | ValidCard$ Card.Land+YouOwn | TriggerZones$ Battlefield | ActivationLimit$ 1 | Execute$ TrigSeekLand | Secondary$ True | TriggerDescription$ When CARDNAME enters and whenever a land card leaves your graveyard, seek a land card. This ability triggers only once each turn. +SVar:TrigSeekLand:DB$ Seek | Type$ Card.Land +T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | ActivationLimit$ 1 | Execute$ TrigSeekNonLand | TriggerDescription$ When CARDNAME enters and whenever a nonland card leaves your graveyard, seek a nonland card. This ability triggers only once each turn. +T:Mode$ ChangesZone | Origin$ Graveyard | Destination$ Any | ValidCard$ Card.nonLand+YouOwn | TriggerZones$ Battlefield | ActivationLimit$ 1 | Execute$ TrigSeekNonLand | Secondary$ True | TriggerDescription$ When CARDNAME enters and whenever a nonland card leaves your graveyard, seek a nonland card. This ability triggers only once each turn. +SVar:TrigSeekNonLand:DB$ Seek | Type$ Card.nonLand Oracle:When Oasis of Renewal enters and whenever a land card leaves your graveyard, seek a land card. This ability triggers only once each turn.\nWhen Oasis of Renewal enters and whenever a nonland card leaves your graveyard, seek a nonland card. This ability triggers only once each turn. diff --git a/forge-gui/res/cardsfolder/v/victor_valgavoths_seneschal.txt b/forge-gui/res/cardsfolder/v/victor_valgavoths_seneschal.txt index 743bb9c4e4e..65800ccee12 100644 --- a/forge-gui/res/cardsfolder/v/victor_valgavoths_seneschal.txt +++ b/forge-gui/res/cardsfolder/v/victor_valgavoths_seneschal.txt @@ -4,12 +4,9 @@ Types:Legendary Creature Human Warlock PT:3/3 T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Enchantment.YouCtrl | TriggerZones$ Battlefield | Execute$ TrigSurveil | TriggerDescription$ Eerie — Whenever an enchantment you control enters and whenever you fully unlock a Room, surveil 2 if this is the first time this ability has resolved this turn. If it's the second time, each opponent discards a card. If it's the third time, put a creature card from a graveyard onto the battlefield under your control. T:Mode$ FullyUnlock | ValidCard$ Card.Room | ValidPlayer$ You | Secondary$ True | Execute$ TrigSurveil | TriggerZones$ Battlefield | TriggerDescription$ Eerie — Whenever an enchantment you control enters and whenever you fully unlock a Room, surveil 2 if this is the first time this ability has resolved this turn. If it's the second time, each opponent discards a card. If it's the third time, put a creature card from a graveyard onto the battlefield under your control. -SVar:TrigSurveil:DB$ Surveil | Amount$ 2 | ConditionCheckSVar$ X | ConditionSVarCompare$ EQ1 | SubAbility$ DBDiscard -SVar:DBDiscard:DB$ Discard | Defined$ Player.Opponent | Mode$ TgtChoose | ConditionCheckSVar$ X | ConditionSVarCompare$ EQ2 | SubAbility$ DBChangeZone -SVar:DBChangeZone:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ChangeType$ Creature | ChangeNum$ 1 | Mandatory$ True | GainControl$ True | ConditionCheckSVar$ X | ConditionSVarCompare$ EQ3 | SelectPrompt$ Select a creature card in a graveyard | Hidden$ True | SubAbility$ DBLog -SVar:DBLog:DB$ StoreSVar | SVar$ X | Type$ CountSVar | Expression$ X/Plus.1 -SVar:X:Number$1 -T:Mode$ Phase | Phase$ Cleanup | TriggerZones$ Battlefield | Execute$ DBCleanup | Static$ True -SVar:DBCleanup:DB$ StoreSVar | SVar$ X | Type$ Number | Expression$ 1 +SVar:TrigSurveil:DB$ Surveil | Amount$ 2 | ConditionCheckSVar$ Resolved | ConditionSVarCompare$ EQ1 | SubAbility$ DBDiscard +SVar:DBDiscard:DB$ Discard | Defined$ Player.Opponent | Mode$ TgtChoose | ConditionCheckSVar$ Resolved | ConditionSVarCompare$ EQ2 | SubAbility$ DBChangeZone +SVar:DBChangeZone:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ChangeType$ Creature | ChangeNum$ 1 | Mandatory$ True | GainControl$ True | ConditionCheckSVar$ Resolved | ConditionSVarCompare$ EQ3 | SelectPrompt$ Select a creature card in a graveyard | Hidden$ True +SVar:Resolved:Count$ResolvedThisTurn DeckNeeds:Type$Enchantment Oracle:Eerie — Whenever an enchantment you control enters and whenever you fully unlock a Room, surveil 2 if this is the first time this ability has resolved this turn. If it's the second time, each opponent discards a card. If it's the third time, put a creature card from a graveyard onto the battlefield under your control.