diff --git a/forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java index bd44c84feb9..cc416780a1e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java @@ -53,6 +53,10 @@ public class CharmEffect extends SpellAbilityEffect { } public static String makeFormatedDescription(SpellAbility sa) { + return makeFormatedDescription(sa, true); + } + + public static String makeFormatedDescription(SpellAbility sa, boolean includeChosen) { Card source = sa.getHostCard(); List list = CharmEffect.makePossibleOptions(sa); @@ -83,7 +87,7 @@ public class CharmEffect extends SpellAbilityEffect { sb.append(oppChooses ? "An opponent chooses " : "Choose "); if (num == min || num == Integer.MAX_VALUE) { - sb.append(Lang.getNumeral(min)); + sb.append(num == 0 ? "up to that many" : Lang.getNumeral(min)); } else if (min == 0 && num == sa.getParam("Choices").split(",").length) { sb.append("any number "); } else if (min == 0) { @@ -137,7 +141,9 @@ public class CharmEffect extends SpellAbilityEffect { } } - if (!list.isEmpty()) { + if (!includeChosen) { + sb.append(num == 1 ? " mode." : " modes."); + } else if (!list.isEmpty()) { if (!repeat && !additionalDesc && !limit && !gameLimit) { sb.append(" \u2014"); } @@ -146,6 +152,7 @@ public class CharmEffect extends SpellAbilityEffect { sb.append("\u2022 ").append(sub.getParam("SpellDescription")); sb.append("\r\n"); } + sb.append("\r\n"); } return sb.toString(); } 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 a6ffb80ed22..871df609cdc 100644 --- a/forge-game/src/main/java/forge/game/trigger/Trigger.java +++ b/forge-game/src/main/java/forge/game/trigger/Trigger.java @@ -17,23 +17,10 @@ */ package forge.game.trigger; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; - -import forge.game.Game; -import forge.game.GameEntity; -import forge.game.GameStage; -import forge.game.IHasSVars; -import forge.game.TriggerReplacementBase; +import forge.game.*; import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityKey; import forge.game.ability.ApiType; @@ -50,6 +37,8 @@ import forge.util.CardTranslation; import forge.util.Lang; import forge.util.TextUtil; +import java.util.*; + /** *

* Abstract Trigger class. Constructed by reflection only @@ -167,6 +156,10 @@ public abstract class Trigger extends TriggerReplacementBase { } public final String replaceAbilityText(final String desc, SpellAbility sa) { + return replaceAbilityText(desc, sa, false); + } + + public final String replaceAbilityText(final String desc, SpellAbility sa, boolean forStack) { String result = desc; // this function is for ABILITY @@ -177,23 +170,41 @@ public abstract class Trigger extends TriggerReplacementBase { sa = getOverridingAbility(); } if (sa != null) { - String saDesc; + String saDesc = ""; + boolean digMore = true; // if sa is a wrapper, get the Wrapped Ability if (sa.isWrapper()) { final WrappedAbility wa = (WrappedAbility) sa; sa = wa.getWrappedAbility(); - // wrapped Charm spells are special, - // only get the selected abilities + // wrapped Charm spells are special, only get the selected abilities (if there are any yet) if (ApiType.Charm.equals(sa.getApi())) { saDesc = sa.getStackDescription(); - } else { - saDesc = sa.toString(); + digMore = false; } - } else if (ApiType.Charm.equals(sa.getApi())) { - // use special formating, can be used in Card Description - saDesc = CharmEffect.makeFormatedDescription(sa); - } else { + } + if (digMore) { // if ABILITY is used, there is probably Charm somewhere + while (sa != null) { + ApiType api = sa.getApi(); + if (ApiType.Charm.equals(api)) { + saDesc = CharmEffect.makeFormatedDescription(sa, !forStack); + break; + } + if (ApiType.ImmediateTrigger.equals(api) || ApiType.DelayedTrigger.equals(api)) { + SpellAbility trigSA = sa.getAdditionalAbility("Execute"); + while (trigSA != null) { + if (ApiType.Charm.equals(trigSA.getApi())) { + saDesc = CharmEffect.makeFormatedDescription(trigSA, !forStack); + break; + } + trigSA = trigSA.getSubAbility(); + } + break; + } + sa = sa.getSubAbility(); + } + } + if (saDesc.equals("")) { // in case we haven't found anything better saDesc = sa.toString(); } // string might have leading whitespace diff --git a/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java b/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java index 034e1e6ef40..7557f73d4ed 100644 --- a/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java +++ b/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java @@ -229,7 +229,8 @@ public class WrappedAbility extends Ability { public String getStackDescription() { final Trigger regtrig = getTrigger(); if (regtrig == null) return ""; - final StringBuilder sb = new StringBuilder(regtrig.replaceAbilityText(regtrig.toString(true), this)); + final StringBuilder sb = + new StringBuilder(regtrig.replaceAbilityText(regtrig.toString(true), this, true)); List allTargets = sa.getAllTargetChoices(); if (!allTargets.isEmpty() && !ApiType.Charm.equals(sa.getApi())) { sb.append(" (Targeting: "); diff --git a/forge-gui/res/cardsfolder/b/bloodthirsty_adversary.txt b/forge-gui/res/cardsfolder/b/bloodthirsty_adversary.txt index f1111b4fffb..a1208e086b1 100644 --- a/forge-gui/res/cardsfolder/b/bloodthirsty_adversary.txt +++ b/forge-gui/res/cardsfolder/b/bloodthirsty_adversary.txt @@ -11,4 +11,5 @@ SVar:DBCopyCast:DB$ Play | Valid$ Card.IsRemembered | ValidZone$ Exile | Control SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:X:Count$TriggerRememberAmount DeckHas:Ability$Counters +AI:RemoveDeck:All Oracle:Haste\nWhen Bloodthirsty Adversary enters the battlefield, you may pay {2}{R} any number of times. When you pay this cost one or more times, put that many +1/+1 counters on Bloodthirsty Adversary, then exile up to that many target instant and/or sorcery cards with mana value 3 or less from your graveyard and copy them. You may cast any number of the copies without paying their mana costs. diff --git a/forge-gui/res/cardsfolder/c/cemetery_desecrator.txt b/forge-gui/res/cardsfolder/c/cemetery_desecrator.txt index 3539dd2e6cb..2fa31b15fc4 100644 --- a/forge-gui/res/cardsfolder/c/cemetery_desecrator.txt +++ b/forge-gui/res/cardsfolder/c/cemetery_desecrator.txt @@ -3,8 +3,8 @@ ManaCost:4 B B Types:Creature Zombie PT:4/4 K:Menace -T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigExile | TriggerDescription$ When CARDNAME enters the battlefield or dies, exile another card from a graveyard. -T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigExile | Secondary$ True | TriggerDescription$ When CARDNAME enters the battlefield or dies, exile another card from a graveyard. +T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigExile | TriggerDescription$ When CARDNAME enters the battlefield or dies, exile another card from a graveyard. When you do, ABILITY +T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigExile | Secondary$ True | TriggerDescription$ When CARDNAME enters the battlefield or dies, exile another card from a graveyard. When you do, ABILITY SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | Hidden$ True | RememberChanged$ True | ChangeType$ Card.Other | ChangeNum$ 1 | Mandatory$ True | AILogic$ ExilePreference:HighestCMC | SubAbility$ DBImmediateTrigger SVar:DBImmediateTrigger:DB$ ImmediateTrigger | ConditionDefined$ Remembered | ConditionPresent$ Card | RememberObjects$ Remembered | SubAbility$ DBCleanup | Execute$ TrigCharm | TriggerDescription$ When you do, ABILITY SVar:TrigCharm:DB$ Charm | Choices$ DBRemoveCounter,DBPump diff --git a/forge-gui/res/cardsfolder/g/guile_sonic_soldier.txt b/forge-gui/res/cardsfolder/g/guile_sonic_soldier.txt index 2cbd06c5d41..1fb2a6ef87b 100644 --- a/forge-gui/res/cardsfolder/g/guile_sonic_soldier.txt +++ b/forge-gui/res/cardsfolder/g/guile_sonic_soldier.txt @@ -7,8 +7,8 @@ T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigCharge | Secondary$ True | SVar:TrigCharge:DB$ AddOrRemoveCounter | Defined$ Self | CounterType$ CHARGE | CounterNum$ 1 | RememberRemovedCards$ True | SubAbility$ DBImmediateTrigger SVar:DBImmediateTrigger:DB$ ImmediateTrigger | ConditionDefined$ Remembered | ConditionPresent$ Card.Self | ConditionCompare$ GE1 | Execute$ TrigCharm | SubAbility$ DBCleanup | TriggerDescription$ When you remove a counter this way, ABILITY SVar:TrigCharm:DB$ Charm | Choices$ DBDamage,DBPump -SVar:DBDamage:DB$ DealDamage | ValidTgts$ Any | NumDmg$ 4 | SpellDescription$ CARDNAME deals 4 damage to any target. -SVar:DBPump:DB$ Pump | Defined$ Self | KW$ Lifelink & Indestructible | SpellDescription$ CARDNAME gains lifelink and indestructible until end of turn. +SVar:DBDamage:DB$ DealDamage | ValidTgts$ Any | NumDmg$ 4 | SpellDescription$ Sonic Boom — CARDNAME deals 4 damage to any target. +SVar:DBPump:DB$ Pump | KW$ Lifelink & Indestructible | SpellDescription$ Flash Kick — CARDNAME gains lifelink and indestructible until end of turn. SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True DeckHas:Ability$Counters|LifeGain Oracle:Whenever Guile, Sonic Soldier enters the battlefield or attacks, put a charge counter on him or remove one from him. When you remove a counter this way, choose one—\n• Sonic Boom — Guile, Sonic Soldier deals 4 damage to any target.\n• Flash Kick — Guile, Sonic Soldier gains lifelink and indestructible until end of turn. diff --git a/forge-gui/res/cardsfolder/p/primal_adversary.txt b/forge-gui/res/cardsfolder/p/primal_adversary.txt index 4e11f174f9d..b8974d20e5a 100644 --- a/forge-gui/res/cardsfolder/p/primal_adversary.txt +++ b/forge-gui/res/cardsfolder/p/primal_adversary.txt @@ -9,4 +9,5 @@ SVar:TrigPutCounter:DB$ PutCounter | CounterType$ P1P1 | CounterNum$ X | SubAbil SVar:DBAnimate:DB$ Animate | TargetMin$ 0 | TargetMax$ X | ValidTgts$ Land.YouCtrl | TgtPrompt$ Select up to that many target lands you control | Power$ 3 | Toughness$ 3 | Types$ Wolf,Creature | Keywords$ Haste | Duration$ Permanent SVar:X:Count$TriggerRememberAmount DeckHas:Ability$Counters +AI:RemoveDeck:All Oracle:Trample\nWhen Primal Adversary enters the battlefield, you may pay {1}{G} any number of times. When you pay this cost one or more times, put that many +1/+1 counters on Primal Adversary, then up to that many target lands you control become 3/3 Wolf creatures with haste that are still lands. diff --git a/forge-gui/res/cardsfolder/s/spectral_adversary.txt b/forge-gui/res/cardsfolder/s/spectral_adversary.txt index ea66850993a..6b5dee295ee 100644 --- a/forge-gui/res/cardsfolder/s/spectral_adversary.txt +++ b/forge-gui/res/cardsfolder/s/spectral_adversary.txt @@ -10,4 +10,5 @@ SVar:TrigPutCounter:DB$ PutCounter | CounterType$ P1P1 | CounterNum$ X | SubAbil SVar:DBPhases:DB$ Phases | ValidTgts$ Creature.Other,Artifact.Other,Enchantment.Other | TgtPrompt$ Select up to that many other target artifacts, creatures, and/or enchantments | TargetMin$ 0 | TargetMax$ X SVar:X:Count$TriggerRememberAmount DeckHas:Ability$Counters +AI:RemoveDeck:All Oracle:Flash\nFlying\nWhen Spectral Adversary enters the battlefield, you may pay {1}{U} any number of times. When you pay this cost one or more times, put that many +1/+1 counters on Spectral Adversary, then up to that many other target artifacts, creatures, and/or enchantments phase out. diff --git a/forge-gui/res/cardsfolder/t/tainted_adversary.txt b/forge-gui/res/cardsfolder/t/tainted_adversary.txt index 3fc6893f907..dc48ec2432a 100644 --- a/forge-gui/res/cardsfolder/t/tainted_adversary.txt +++ b/forge-gui/res/cardsfolder/t/tainted_adversary.txt @@ -9,4 +9,5 @@ SVar:TrigPutCounter:DB$ PutCounter | CounterType$ P1P1 | CounterNum$ X | SubAbil SVar:DBToken:DB$ Token | TokenScript$ b_2_2_zombie_decayed | TokenAmount$ SVar$X/Twice SVar:X:Count$TriggerRememberAmount DeckHas:Ability$Token|Counters +AI:RemoveDeck:All Oracle:Deathtouch\nWhen Tainted Adversary enters the battlefield, you may pay {2}{B} any number of times. When you pay this cost one or more times, put that many +1/+1 counters on Tainted Adversary, then create twice that many black 2/2 Zombie creature tokens with decayed. (A creature with decayed can't block. When it attacks, sacrifice it at end of combat.) diff --git a/forge-gui/res/cardsfolder/upcoming/tranquil_frillback.txt b/forge-gui/res/cardsfolder/upcoming/tranquil_frillback.txt new file mode 100644 index 00000000000..61ff35c8b16 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/tranquil_frillback.txt @@ -0,0 +1,14 @@ +Name:Tranquil Frillback +ManaCost:2 G +Types:Creature Dinosaur +PT:3/3 +T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigPay | TriggerDescription$ When CARDNAME enters the battlefield, you may pay {G} up to three times. When you pay this cost one or more times, ABILITY +SVar:TrigPay:AB$ ImmediateTrigger | Cost$ Mana | Announce$ NumTimes | AnnounceMax$ 3 | ConditionCheckSVar$ NumTimes | ConditionSVarCompare$ GE1 | RememberSVarAmount$ NumTimes | Execute$ TrigCharm | TriggerDescription$ When you pay this cost one or more times, ABILITY +SVar:TrigCharm:DB$ Charm | MinCharmNum$ 0 | CharmNum$ X | Choices$ DestroyAE,ExileGrave,GainLife +SVar:DestroyAE:DB$ Destroy | ValidTgts$ Artifact,Enchantment | TgtPrompt$ Select target artifact or enchantment | SpellDescription$ Destroy target artifact or enchantment. +SVar:ExileGrave:DB$ ChangeZoneAll | ValidTgts$ Player | Origin$ Graveyard | Destination$ Exile | ChangeType$ Card | SpellDescription$ Exile target player's graveyard. +SVar:GainLife:DB$ GainLife | LifeAmount$ 4 | SpellDescription$ You gain 4 life. +SVar:X:Count$TriggerRememberAmount +DeckHas:Ability$LifeGain +AI:RemoveDeck:All +Oracle:When Tranquil Frillback enters the battlefield, you may pay {G} up to three times. When you pay this cost one or more times, choose up to that many —\n• Destroy target artifact or enchantment.\n• Exile target player's graveyard.\n• You gain 4 life. diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 3690df4b2ed..70a8ed5cd4f 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -436,6 +436,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont @Override public Integer announceRequirements(final SpellAbility ability, final String announce) { + final Card host = ability.getHostCard(); int max = Integer.MAX_VALUE; boolean canChooseZero = true; Cost cost = ability.getPayCosts(); @@ -443,7 +444,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont if ("X".equals(announce)) { canChooseZero = !ability.hasParam("XCantBe0"); if (ability.hasParam("XMaxLimit")) { - max = Math.min(max, AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("XMaxLimit"), ability)); + max = Math.min(max, AbilityUtils.calculateAmount(host, ability.getParam("XMaxLimit"), ability)); } if (cost != null) { Integer costX = cost.getMaxForNonManaX(ability, player, false); @@ -457,6 +458,10 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont } final int min = canChooseZero ? 0 : 1; + if (ability.hasParam("AnnounceMax")) { + max = Math.min(max, AbilityUtils.calculateAmount(host, ability.getParam("AnnounceMax"), ability)); + } + if (ability.usesTargeting()) { // if announce is used as min targets, check what the max possible number would be if (announce.equals(ability.getTargetRestrictions().getMinTargets())) { @@ -471,14 +476,14 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont ability.getParamOrDefault("AnnounceTitle", announce); if (cost.isMandatory()) { return chooseNumber(ability, localizer.getMessage("lblChooseAnnounceForCard", announceTitle, - CardTranslation.getTranslatedName(ability.getHostCard().getName())), min, max); + CardTranslation.getTranslatedName(host.getName())), min, max); } if ("NumTimes".equals(announce)) { return getGui().getInteger(localizer.getMessage("lblHowManyTimesToPay", ability.getPayCosts().getTotalMana(), - CardTranslation.getTranslatedName(ability.getHostCard().getName())), min, max, min + 9); + CardTranslation.getTranslatedName(host.getName())), min, max, min + 9); } return getGui().getInteger(localizer.getMessage("lblChooseAnnounceForCard", announceTitle, - CardTranslation.getTranslatedName(ability.getHostCard().getName())), min, max, min + 9); + CardTranslation.getTranslatedName(host.getName())), min, max, min + 9); } @Override