Fix AI can't use special mana abilities of e.g. Faeburrow Elder

This commit is contained in:
Cees Timmerman
2025-11-16 21:46:09 +01:00
committed by GitHub
parent 8129946fdf
commit 2117bf6edf
11 changed files with 112 additions and 115 deletions

View File

@@ -45,6 +45,9 @@ In IntelliJ, if the SDK Manager is not already running, go to Tools > Android >
- Android SDK Build-tools 35.0.0
- Android 15 (API 35) SDK Platform
> [!CAUTION]
> Be careful about using unsupported api calls e.g. ``StringBuilder.isEmpty()``. Google's documentation for these is sometimes inaccurate.
### Proguard update
Standalone Proguard 7.6.0 is included with the project (proguard.jar) under forge-gui-android > tools and supports up to Java 23 (latest android uses Java 17).

View File

@@ -66,10 +66,8 @@ public class UntapAi extends SpellAbilityAi {
if (pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai)) {
// If the defined card is tapped, or if there are no defined cards, we can play this ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// Otherwise, we can't play this ability
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
@Override

View File

@@ -57,7 +57,6 @@ import java.util.EnumSet;
import java.util.List;
import java.util.Map;
/**
* <p>
* GameActionUtil class.
@@ -859,8 +858,6 @@ public final class GameActionUtil {
}
} else if (sa.getApi() == ApiType.ManaReflected) {
baseMana = abMana.getExpressChoice();
} else if (abMana.isSpecialMana()) {
baseMana = abMana.getExpressChoice();
} else {
baseMana = abMana.mana(sa);
}

View File

@@ -5,6 +5,7 @@ import static forge.util.TextUtil.toManaString;
import java.util.List;
import java.util.Map;
import forge.game.card.CardUtil;
import forge.util.Lang;
import org.apache.commons.lang3.StringUtils;
@@ -17,14 +18,11 @@ import forge.game.GameActionUtil;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Localizer;
import io.sentry.Breadcrumb;
import io.sentry.Sentry;
@@ -41,8 +39,8 @@ public class ManaEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard();
final Game game = card.getGame();
final Card host = sa.getHostCard();
final Game game = host.getGame();
final AbilityManaPart abMana = sa.getManaPart();
final List<Player> tgtPlayers = getDefinedPlayersOrTargeted(sa);
final Player activator = sa.getActivatingPlayer();
@@ -63,13 +61,13 @@ public class ManaEffect extends SpellAbilityEffect {
final Player chooser;
if (sa.hasParam("Chooser")) {
chooser = AbilityUtils.getDefinedPlayers(card, sa.getParam("Chooser"), sa).get(0);
chooser = AbilityUtils.getDefinedPlayers(host, sa.getParam("Chooser"), sa).get(0);
} else {
chooser = p;
}
if (abMana.isComboMana()) {
int amount = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(card, sa.getParam("Amount"), sa) : 1;
int amount = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(host, sa.getParam("Amount"), sa) : 1;
if (amount <= 0)
continue;
@@ -117,7 +115,7 @@ public class ManaEffect extends SpellAbilityEffect {
byte chosenColor = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblSelectManaProduce"), sa,
differentChoice && (colorsNeeded == null || colorsNeeded.length <= nMana) ? fullOptions : colorOptions);
if (chosenColor == 0)
throw new RuntimeException("ManaEffect::resolve() /*combo mana*/ - " + p + " color mana choice is empty for " + card.getName());
throw new RuntimeException("ManaEffect::resolve() /*combo mana*/ - " + p + " color mana choice is empty for " + host.getName());
if (differentChoice) {
fullOptions = ColorSet.fromMask(fullOptions.getColor() - chosenColor);
@@ -159,19 +157,52 @@ public class ManaEffect extends SpellAbilityEffect {
colorMenu = mask == 0 ? ColorSet.WUBRG : ColorSet.fromMask(mask);
byte val = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblSelectManaProduce"), sa, colorMenu);
if (0 == val) {
throw new RuntimeException("ManaEffect::resolve() /*any mana*/ - " + p + " color mana choice is empty for " + card.getName());
throw new RuntimeException("ManaEffect::resolve() /*any mana*/ - " + p + " color mana choice is empty for " + host.getName());
}
game.getAction().notifyOfValue(sa, card, MagicColor.toSymbol(val), p);
game.getAction().notifyOfValue(sa, host, MagicColor.toSymbol(val), p);
abMana.setExpressChoice(MagicColor.toShortString(val));
}
else if (abMana.isSpecialMana()) {
String type = abMana.getOrigProduced().split("Special ")[1];
handleSpecialMana(chooser, abMana, sa, true);
}
String mana = GameActionUtil.generatedMana(sa);
// this can happen when mana is based on criteria that didn't match
if (mana.isEmpty()) {
String msg = "AbilityFactoryMana::manaResolve() - special mana effect is empty for";
Breadcrumb bread = new Breadcrumb(msg);
bread.setData("Card", host.getName());
bread.setData("SA", sa.toString());
Sentry.addBreadcrumb(bread);
if (type.equals("EnchantedManaCost")) {
Card enchanted = card.getEnchantingCard();
if (enchanted == null)
continue;
}
producedMana.append(abMana.produceMana(mana, p, sa));
}
// Only clear express choice after mana has been produced
abMana.clearExpressChoice();
abMana.tapsForMana(sa.getRootAbility(), producedMana.toString());
if (sa.isKeyword(Keyword.FIREBENDING)) {
activator.triggerElementalBend(TriggerType.Firebend);
}
}
public static void handleSpecialMana(Player chooser, AbilityManaPart abMana, SpellAbility sa, boolean resolve) {
String type = abMana.getOrigProduced().split("Special ")[1];
Card host = sa.getHostCard();
if (resolve) {
if (type.equals("EnchantedManaCost")) {
Card enchanted = host.getEnchantingCard();
if (enchanted == null)
return;
StringBuilder sb = new StringBuilder();
int generic = enchanted.getManaCost().getGenericCost();
@@ -182,13 +213,12 @@ public class ManaEffect extends SpellAbilityEffect {
if (cs.isColorless())
continue;
if (s.isOr2Generic()) { // CR 106.8
chosenColor = chooser.getController().chooseColorAllowColorless(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), card, cs);
chosenColor = chooser.getController().chooseColorAllowColorless(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), host, cs);
if (chosenColor == MagicColor.COLORLESS) {
generic += 2;
continue;
}
}
else if (cs.isMonoColor())
} else if (cs.isMonoColor())
chosenColor = s.getColorMask();
else /* (cs.isMulticolor()) */ {
chosenColor = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
@@ -201,36 +231,10 @@ public class ManaEffect extends SpellAbilityEffect {
}
abMana.setExpressChoice(sb.toString().trim());
} else if (type.equals("LastNotedType")) {
final StringBuilder sb = new StringBuilder();
int nMana = 0;
for (Object o : card.getRemembered()) {
if (o instanceof String) {
sb.append(o);
nMana++;
}
}
if (nMana == 0) {
return;
}
abMana.setExpressChoice(sb.toString());
} else if (type.startsWith("EachColorAmong")) {
final String res = type.split("_")[1];
final boolean defined = type.startsWith("EachColorAmongDefined");
final ZoneType zone = defined || type.startsWith("EachColorAmong_") ? ZoneType.Battlefield :
ZoneType.smartValueOf(type.split("_")[0].substring(14));
final CardCollection list = defined ? AbilityUtils.getDefinedCards(card, res, sa) :
CardLists.getValidCards(card.getGame().getCardsIn(zone), res, activator, card, sa);
byte colors = 0;
for (Card c : list) {
colors |= c.getColor().getColor();
}
if (colors == 0) return;
abMana.setExpressChoice(ColorSet.fromMask(colors));
} else if (type.startsWith("EachColoredManaSymbol")) {
final String res = type.split("_")[1];
StringBuilder sb = new StringBuilder();
for (Card c : AbilityUtils.getDefinedCards(card, res, sa)) {
for (Card c : AbilityUtils.getDefinedCards(host, res, sa)) {
for (ManaCostShard s : c.getManaCost()) {
ColorSet cs = ColorSet.fromMask(s.getColorMask());
if (cs.isColorless())
@@ -248,36 +252,28 @@ public class ManaEffect extends SpellAbilityEffect {
} else if (type.startsWith("DoubleManaInPool")) {
StringBuilder sb = new StringBuilder();
for (byte color : ManaAtom.MANATYPES) {
sb.append(StringUtils.repeat(MagicColor.toShortString(color) + " ", p.getManaPool().getAmountOfColor(color)));
sb.append(StringUtils.repeat(MagicColor.toShortString(color) + " ", chooser.getManaPool().getAmountOfColor(color)));
}
abMana.setExpressChoice(sb.toString().trim());
}
} else if (type.equals("LastNotedType")) {
// Jeweled Lotus
final StringBuilder sb = new StringBuilder();
for (Object o : host.getRemembered()) {
if (o instanceof String) {
sb.append(o);
}
String mana = GameActionUtil.generatedMana(sa);
// this can happen when mana is based on criteria that didn't match
}
String mana = sb.toString();
if (mana.isEmpty()) {
String msg = "AbilityFactoryMana::manaResolve() - special mana effect is empty for";
Breadcrumb bread = new Breadcrumb(msg);
bread.setData("Card", card.getName());
bread.setData("SA", sa.toString());
Sentry.addBreadcrumb(bread);
continue;
return;
}
producedMana.append(abMana.produceMana(mana, p, sa));
}
// Only clear express choice after mana has been produced
abMana.clearExpressChoice();
abMana.tapsForMana(sa.getRootAbility(), producedMana.toString());
if (sa.isKeyword(Keyword.FIREBENDING)) {
activator.triggerElementalBend(TriggerType.Firebend);
abMana.setExpressChoice(mana);
} else if (type.startsWith("EachColorAmong")) {
final String res = type.split("_")[1];
ColorSet colors = CardUtil.getColorsFromCards(AbilityUtils.getDefinedCards(host, res, sa));
if (colors.isColorless()) return;
abMana.setExpressChoice(colors);
}
}

View File

@@ -30,6 +30,7 @@ import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.SpellAbilityEffect;
import forge.game.ability.effects.ManaEffect;
import forge.game.card.Card;
import forge.game.card.CardUtil;
import forge.game.cost.Cost;
@@ -515,7 +516,11 @@ public class AbilityManaPart implements java.io.Serializable {
}
String produced = this.getOrigProduced();
if (produced.contains("Chosen")) {
produced = produced.replace("Chosen", getChosenColor(sa, sa.getHostCard().getChosenColors()));
produced = produced.replace("Chosen", getChosenColor(sa));
}
if (isSpecialMana()) {
ManaEffect.handleSpecialMana(sa.getActivatingPlayer(), this, sa, false);
produced = getExpressChoice();
}
return produced;
}
@@ -651,7 +656,7 @@ public class AbilityManaPart implements java.io.Serializable {
}
// replace Chosen for Combo colors
if (origProduced.contains("Chosen")) {
origProduced = origProduced.replace("Chosen", getChosenColor(sa, sa.getHostCard().getChosenColors()));
origProduced = origProduced.replace("Chosen", getChosenColor(sa));
}
// replace Chosen for Spire colors
if (origProduced.contains("ColorID")) {
@@ -701,14 +706,14 @@ public class AbilityManaPart implements java.io.Serializable {
return sb.length() == 0 ? "" : sb.substring(0, sb.length() - 1);
}
public String getChosenColor(SpellAbility sa, Iterable<String> colors) {
public String getChosenColor(SpellAbility sa) {
if (sa == null) {
return "";
}
Card card = sa.getHostCard();
if (card != null) {
StringBuilder values = new StringBuilder();
for (String c : colors) {
for (String c : card.getChosenColors()) {
values.append(MagicColor.toShortString(c)).append(" ");
}
return values.toString().trim();

View File

@@ -2,6 +2,6 @@ Name:Bloom Tender
ManaCost:1 G
Types:Creature Elf Druid
PT:1/1
A:AB$ Mana | Cost$ T | Produced$ Special EachColorAmong_Permanent.YouCtrl | SpellDescription$ For each color among permanents you control, add one mana of that color.
A:AB$ Mana | Cost$ T | Produced$ Special EachColorAmong_Valid Permanent.YouCtrl | SpellDescription$ For each color among permanents you control, add one mana of that color.
AI:RemoveDeck:All
Oracle:{T}: For each color among permanents you control, add one mana of that color.

View File

@@ -5,6 +5,6 @@ PT:0/0
K:Vigilance
S:Mode$ Continuous | Affected$ Card.Self | AddPower$ X | AddToughness$ X | Description$ CARDNAME gets +1/+1 for each color among permanents you control.
SVar:X:Count$Valid Permanent.YouCtrl$Colors
A:AB$ Mana | Cost$ T | Produced$ Special EachColorAmong_Permanent.YouCtrl | SpellDescription$ For each color among permanents you control, add one mana of that color.
AI:RemoveDeck:All
A:AB$ Mana | Cost$ T | Produced$ Special EachColorAmong_Valid Permanent.YouCtrl | SpellDescription$ For each color among permanents you control, add one mana of that color.
SVar:NoZeroToughnessAI:True
Oracle:Vigilance\nFaeburrow Elder gets +1/+1 for each color among permanents you control.\n{T}: For each color among permanents you control, add one mana of that color.

View File

@@ -4,5 +4,4 @@ Types:Artifact
A:AB$ PutCounter | Cost$ 1 T | RememberCostMana$ True | CounterType$ CHARGE | CounterNum$ 1 | CheckSVar$ X | SVarCompare$ EQ0 | SpellDescription$ Put a charge counter on CARDNAME. Note the type of mana spent to pay this activation cost. Activate only if there are no charge counters on CARDNAME.
SVar:X:Count$CardCounters.CHARGE
A:AB$ Mana | Cost$ T SubCounter<1/CHARGE> | Produced$ Special LastNotedType | SpellDescription$ Add one mana of CARDNAME's last noted type.
AI:RemoveDeck:All
Oracle:{1}, {T}: Put a charge counter on Jeweled Amulet. Note the type of mana spent to pay this activation cost. Activate only if there are no charge counters on Jeweled Amulet.\n{T}, Remove a charge counter from Jeweled Amulet: Add one mana of Jeweled Amulet's last noted type.

View File

@@ -21,5 +21,5 @@ K:Vigilance
K:Haste
S:Mode$ Continuous | CharacteristicDefining$ True | SetPower$ X | SetToughness$ X | Description$ CARDNAME's power and toughness are each equal to the number of colors among the exiled cards used to craft it.
SVar:X:ExiledWith$Colors
A:AB$ Mana | Cost$ T | Produced$ Special EachColorAmongDefined_ExiledWith | SpellDescription$ For each color among the exiled cards used to craft CARDNAME, add one mana of that color.
A:AB$ Mana | Cost$ T | Produced$ Special EachColorAmong_ExiledWith | SpellDescription$ For each color among the exiled cards used to craft CARDNAME, add one mana of that color.
Oracle:Flying, vigilance, haste\nSunbird Effigy's power and toughness are each equal to the number of colors among the exiled cards used to craft it.\n{T}: For each color among the exiled cards used to craft Sunbird Effigy, add one mana of that color.

View File

@@ -6,5 +6,5 @@ SVar:ETBTapped:DB$ Tap | Defined$ Self | ETB$ True
K:ETBReplacement:Other:ChooseColor
SVar:ChooseColor:DB$ ChooseColor | Defined$ You | AILogic$ MostProminentInComputerDeck | SpellDescription$ As CARDNAME enters, choose a color.
A:AB$ Mana | Cost$ T | Produced$ Chosen | SpellDescription$ Add one mana of the chosen color.
A:AB$ Mana | Cost$ 1 T | Produced$ Special EachColorAmong_Permanent.YouCtrl+MonoColor | SpellDescription$ For each color among monocolored permanents you control, add one mana of that color.
A:AB$ Mana | Cost$ 1 T | Produced$ Special EachColorAmong_Valid Permanent.YouCtrl+MonoColor | SpellDescription$ For each color among monocolored permanents you control, add one mana of that color.
Oracle:Tarnation Vista enters tapped. As it enters, choose a color.\n{T}: Add one mana of the chosen color.\n{1}, {T}: For each color among monocolored permanents you control, add one mana of that color.

View File

@@ -4,6 +4,5 @@ Types:Creature Elf Druid
PT:2/2
A:AB$ Mana | Cost$ T | Produced$ Any | Amount$ X | SpellDescription$ Add X mana of any one color, where X is the number of Elves on the battlefield.
SVar:X:Count$Valid Elf
AI:RemoveDeck:All
AI:RemoveDeck:Random
DeckHints:Type$Elf
Oracle:{T}: Add X mana of any one color, where X is the number of Elves on the battlefield.