Merge branch 'Card-Forge:master' into master

This commit is contained in:
schnautzr
2023-05-24 20:22:05 -05:00
committed by GitHub
11 changed files with 76 additions and 34 deletions

View File

@@ -53,6 +53,10 @@ public class CharmEffect extends SpellAbilityEffect {
} }
public static String makeFormatedDescription(SpellAbility sa) { public static String makeFormatedDescription(SpellAbility sa) {
return makeFormatedDescription(sa, true);
}
public static String makeFormatedDescription(SpellAbility sa, boolean includeChosen) {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
List<AbilitySub> list = CharmEffect.makePossibleOptions(sa); List<AbilitySub> list = CharmEffect.makePossibleOptions(sa);
@@ -83,7 +87,7 @@ public class CharmEffect extends SpellAbilityEffect {
sb.append(oppChooses ? "An opponent chooses " : "Choose "); sb.append(oppChooses ? "An opponent chooses " : "Choose ");
if (num == min || num == Integer.MAX_VALUE) { 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) { } else if (min == 0 && num == sa.getParam("Choices").split(",").length) {
sb.append("any number "); sb.append("any number ");
} else if (min == 0) { } 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) { if (!repeat && !additionalDesc && !limit && !gameLimit) {
sb.append(" \u2014"); sb.append(" \u2014");
} }
@@ -146,6 +152,7 @@ public class CharmEffect extends SpellAbilityEffect {
sb.append("\u2022 ").append(sub.getParam("SpellDescription")); sb.append("\u2022 ").append(sub.getParam("SpellDescription"));
sb.append("\r\n"); sb.append("\r\n");
} }
sb.append("\r\n");
} }
return sb.toString(); return sb.toString();
} }

View File

@@ -17,23 +17,10 @@
*/ */
package forge.game.trigger; 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.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import forge.game.*;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameStage;
import forge.game.IHasSVars;
import forge.game.TriggerReplacementBase;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
@@ -50,6 +37,8 @@ import forge.util.CardTranslation;
import forge.util.Lang; import forge.util.Lang;
import forge.util.TextUtil; import forge.util.TextUtil;
import java.util.*;
/** /**
* <p> * <p>
* Abstract Trigger class. Constructed by reflection only * 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) { 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; String result = desc;
// this function is for ABILITY // this function is for ABILITY
@@ -177,23 +170,41 @@ public abstract class Trigger extends TriggerReplacementBase {
sa = getOverridingAbility(); sa = getOverridingAbility();
} }
if (sa != null) { if (sa != null) {
String saDesc; String saDesc = "";
boolean digMore = true;
// if sa is a wrapper, get the Wrapped Ability // if sa is a wrapper, get the Wrapped Ability
if (sa.isWrapper()) { if (sa.isWrapper()) {
final WrappedAbility wa = (WrappedAbility) sa; final WrappedAbility wa = (WrappedAbility) sa;
sa = wa.getWrappedAbility(); sa = wa.getWrappedAbility();
// wrapped Charm spells are special, // wrapped Charm spells are special, only get the selected abilities (if there are any yet)
// only get the selected abilities
if (ApiType.Charm.equals(sa.getApi())) { if (ApiType.Charm.equals(sa.getApi())) {
saDesc = sa.getStackDescription(); saDesc = sa.getStackDescription();
} else { digMore = false;
saDesc = sa.toString();
} }
} else if (ApiType.Charm.equals(sa.getApi())) { }
// use special formating, can be used in Card Description if (digMore) { // if ABILITY is used, there is probably Charm somewhere
saDesc = CharmEffect.makeFormatedDescription(sa); while (sa != null) {
} else { 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(); saDesc = sa.toString();
} }
// string might have leading whitespace // string might have leading whitespace

View File

@@ -229,7 +229,8 @@ public class WrappedAbility extends Ability {
public String getStackDescription() { public String getStackDescription() {
final Trigger regtrig = getTrigger(); final Trigger regtrig = getTrigger();
if (regtrig == null) return ""; 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<TargetChoices> allTargets = sa.getAllTargetChoices(); List<TargetChoices> allTargets = sa.getAllTargetChoices();
if (!allTargets.isEmpty() && !ApiType.Charm.equals(sa.getApi())) { if (!allTargets.isEmpty() && !ApiType.Charm.equals(sa.getApi())) {
sb.append(" (Targeting: "); sb.append(" (Targeting: ");

View File

@@ -11,4 +11,5 @@ SVar:DBCopyCast:DB$ Play | Valid$ Card.IsRemembered | ValidZone$ Exile | Control
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:X:Count$TriggerRememberAmount SVar:X:Count$TriggerRememberAmount
DeckHas:Ability$Counters 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. 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.

View File

@@ -3,8 +3,8 @@ ManaCost:4 B B
Types:Creature Zombie Types:Creature Zombie
PT:4/4 PT:4/4
K:Menace 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$ 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. 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: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: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 SVar:TrigCharm:DB$ Charm | Choices$ DBRemoveCounter,DBPump

View File

@@ -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: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: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:TrigCharm:DB$ Charm | Choices$ DBDamage,DBPump
SVar:DBDamage:DB$ DealDamage | ValidTgts$ Any | NumDmg$ 4 | SpellDescription$ CARDNAME deals 4 damage to any target. SVar:DBDamage:DB$ DealDamage | ValidTgts$ Any | NumDmg$ 4 | SpellDescription$ Sonic Boom — 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:DBPump:DB$ Pump | KW$ Lifelink & Indestructible | SpellDescription$ Flash Kick — CARDNAME gains lifelink and indestructible until end of turn.
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
DeckHas:Ability$Counters|LifeGain 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. 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.

View File

@@ -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: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 SVar:X:Count$TriggerRememberAmount
DeckHas:Ability$Counters 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. 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.

View File

@@ -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: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 SVar:X:Count$TriggerRememberAmount
DeckHas:Ability$Counters 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. 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.

View File

@@ -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:DBToken:DB$ Token | TokenScript$ b_2_2_zombie_decayed | TokenAmount$ SVar$X/Twice
SVar:X:Count$TriggerRememberAmount SVar:X:Count$TriggerRememberAmount
DeckHas:Ability$Token|Counters 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.) 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.)

View File

@@ -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<G\NumTimes> | 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.

View File

@@ -436,6 +436,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
@Override @Override
public Integer announceRequirements(final SpellAbility ability, final String announce) { public Integer announceRequirements(final SpellAbility ability, final String announce) {
final Card host = ability.getHostCard();
int max = Integer.MAX_VALUE; int max = Integer.MAX_VALUE;
boolean canChooseZero = true; boolean canChooseZero = true;
Cost cost = ability.getPayCosts(); Cost cost = ability.getPayCosts();
@@ -443,7 +444,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
if ("X".equals(announce)) { if ("X".equals(announce)) {
canChooseZero = !ability.hasParam("XCantBe0"); canChooseZero = !ability.hasParam("XCantBe0");
if (ability.hasParam("XMaxLimit")) { 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) { if (cost != null) {
Integer costX = cost.getMaxForNonManaX(ability, player, false); Integer costX = cost.getMaxForNonManaX(ability, player, false);
@@ -457,6 +458,10 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
} }
final int min = canChooseZero ? 0 : 1; 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 (ability.usesTargeting()) {
// if announce is used as min targets, check what the max possible number would be // if announce is used as min targets, check what the max possible number would be
if (announce.equals(ability.getTargetRestrictions().getMinTargets())) { if (announce.equals(ability.getTargetRestrictions().getMinTargets())) {
@@ -471,14 +476,14 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
ability.getParamOrDefault("AnnounceTitle", announce); ability.getParamOrDefault("AnnounceTitle", announce);
if (cost.isMandatory()) { if (cost.isMandatory()) {
return chooseNumber(ability, localizer.getMessage("lblChooseAnnounceForCard", announceTitle, return chooseNumber(ability, localizer.getMessage("lblChooseAnnounceForCard", announceTitle,
CardTranslation.getTranslatedName(ability.getHostCard().getName())), min, max); CardTranslation.getTranslatedName(host.getName())), min, max);
} }
if ("NumTimes".equals(announce)) { if ("NumTimes".equals(announce)) {
return getGui().getInteger(localizer.getMessage("lblHowManyTimesToPay", ability.getPayCosts().getTotalMana(), 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, 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 @Override