diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f2c4aad3fd0..fdc81309a1f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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).
diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java
index 449afdb1f3f..984901464d6 100644
--- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java
@@ -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
diff --git a/forge-game/src/main/java/forge/game/GameActionUtil.java b/forge-game/src/main/java/forge/game/GameActionUtil.java
index ff73a49b865..0680c34d1a7 100644
--- a/forge-game/src/main/java/forge/game/GameActionUtil.java
+++ b/forge-game/src/main/java/forge/game/GameActionUtil.java
@@ -57,7 +57,6 @@ import java.util.EnumSet;
import java.util.List;
import java.util.Map;
-
/**
*
* 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);
}
diff --git a/forge-game/src/main/java/forge/game/ability/effects/ManaEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ManaEffect.java
index b21b252e4cb..896972f0a73 100644
--- a/forge-game/src/main/java/forge/game/ability/effects/ManaEffect.java
+++ b/forge-game/src/main/java/forge/game/ability/effects/ManaEffect.java
@@ -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 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,99 +157,14 @@ 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];
-
- if (type.equals("EnchantedManaCost")) {
- Card enchanted = card.getEnchantingCard();
- if (enchanted == null)
- continue;
-
- StringBuilder sb = new StringBuilder();
- int generic = enchanted.getManaCost().getGenericCost();
-
- for (ManaCostShard s : enchanted.getManaCost()) {
- ColorSet cs = ColorSet.fromMask(s.getColorMask());
- byte chosenColor;
- if (cs.isColorless())
- continue;
- if (s.isOr2Generic()) { // CR 106.8
- chosenColor = chooser.getController().chooseColorAllowColorless(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), card, cs);
- if (chosenColor == MagicColor.COLORLESS) {
- generic += 2;
- continue;
- }
- }
- else if (cs.isMonoColor())
- chosenColor = s.getColorMask();
- else /* (cs.isMulticolor()) */ {
- chosenColor = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
- }
- sb.append(MagicColor.toShortString(chosenColor));
- sb.append(' ');
- }
- if (generic > 0) {
- sb.append(generic);
- }
-
- 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 (ManaCostShard s : c.getManaCost()) {
- ColorSet cs = ColorSet.fromMask(s.getColorMask());
- if (cs.isColorless())
- continue;
- sb.append(' ');
- if (cs.isMonoColor())
- sb.append(MagicColor.toShortString(s.getColorMask()));
- else /* (cs.isMulticolor()) */ {
- byte chosenColor = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
- sb.append(MagicColor.toShortString(chosenColor));
- }
- }
- }
- abMana.setExpressChoice(sb.toString().trim());
- } 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)));
- }
- abMana.setExpressChoice(sb.toString().trim());
- }
+ handleSpecialMana(chooser, abMana, sa, true);
}
String mana = GameActionUtil.generatedMana(sa);
@@ -261,7 +174,7 @@ public class ManaEffect extends SpellAbilityEffect {
String msg = "AbilityFactoryMana::manaResolve() - special mana effect is empty for";
Breadcrumb bread = new Breadcrumb(msg);
- bread.setData("Card", card.getName());
+ bread.setData("Card", host.getName());
bread.setData("SA", sa.toString());
Sentry.addBreadcrumb(bread);
@@ -281,6 +194,89 @@ public class ManaEffect extends SpellAbilityEffect {
}
}
+ 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();
+
+ for (ManaCostShard s : enchanted.getManaCost()) {
+ ColorSet cs = ColorSet.fromMask(s.getColorMask());
+ byte chosenColor;
+ if (cs.isColorless())
+ continue;
+ if (s.isOr2Generic()) { // CR 106.8
+ chosenColor = chooser.getController().chooseColorAllowColorless(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), host, cs);
+ if (chosenColor == MagicColor.COLORLESS) {
+ generic += 2;
+ continue;
+ }
+ } else if (cs.isMonoColor())
+ chosenColor = s.getColorMask();
+ else /* (cs.isMulticolor()) */ {
+ chosenColor = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
+ }
+ sb.append(MagicColor.toShortString(chosenColor));
+ sb.append(' ');
+ }
+ if (generic > 0) {
+ sb.append(generic);
+ }
+
+ abMana.setExpressChoice(sb.toString().trim());
+ } else if (type.startsWith("EachColoredManaSymbol")) {
+ final String res = type.split("_")[1];
+ StringBuilder sb = new StringBuilder();
+ for (Card c : AbilityUtils.getDefinedCards(host, res, sa)) {
+ for (ManaCostShard s : c.getManaCost()) {
+ ColorSet cs = ColorSet.fromMask(s.getColorMask());
+ if (cs.isColorless())
+ continue;
+ sb.append(' ');
+ if (cs.isMonoColor())
+ sb.append(MagicColor.toShortString(s.getColorMask()));
+ else /* (cs.isMulticolor()) */ {
+ byte chosenColor = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblChooseSingleColorFromTarget", s.toString()), sa, cs);
+ sb.append(MagicColor.toShortString(chosenColor));
+ }
+ }
+ }
+ abMana.setExpressChoice(sb.toString().trim());
+ } else if (type.startsWith("DoubleManaInPool")) {
+ StringBuilder sb = new StringBuilder();
+ for (byte color : ManaAtom.MANATYPES) {
+ 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 = sb.toString();
+ if (mana.isEmpty()) {
+ return;
+ }
+ 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);
+ }
+ }
+
/**
*
* manaStackDescription.
diff --git a/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java b/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java
index 73138f215dc..cf411263458 100644
--- a/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java
+++ b/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java
@@ -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 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();
diff --git a/forge-gui/res/cardsfolder/b/bloom_tender.txt b/forge-gui/res/cardsfolder/b/bloom_tender.txt
index dd8c7802738..cedb1a37af8 100644
--- a/forge-gui/res/cardsfolder/b/bloom_tender.txt
+++ b/forge-gui/res/cardsfolder/b/bloom_tender.txt
@@ -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.
diff --git a/forge-gui/res/cardsfolder/f/faeburrow_elder.txt b/forge-gui/res/cardsfolder/f/faeburrow_elder.txt
index f0005710a4f..b92a469ad37 100644
--- a/forge-gui/res/cardsfolder/f/faeburrow_elder.txt
+++ b/forge-gui/res/cardsfolder/f/faeburrow_elder.txt
@@ -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.
diff --git a/forge-gui/res/cardsfolder/j/jeweled_amulet.txt b/forge-gui/res/cardsfolder/j/jeweled_amulet.txt
index 79ce68a98b1..3bb4a6282d3 100644
--- a/forge-gui/res/cardsfolder/j/jeweled_amulet.txt
+++ b/forge-gui/res/cardsfolder/j/jeweled_amulet.txt
@@ -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.
diff --git a/forge-gui/res/cardsfolder/s/sunbird_standard_sunbird_effigy.txt b/forge-gui/res/cardsfolder/s/sunbird_standard_sunbird_effigy.txt
index 9dc3d9aaefd..0fdd34a170b 100644
--- a/forge-gui/res/cardsfolder/s/sunbird_standard_sunbird_effigy.txt
+++ b/forge-gui/res/cardsfolder/s/sunbird_standard_sunbird_effigy.txt
@@ -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.
diff --git a/forge-gui/res/cardsfolder/t/tarnation_vista.txt b/forge-gui/res/cardsfolder/t/tarnation_vista.txt
index 0cd05396eb9..8873498bea2 100644
--- a/forge-gui/res/cardsfolder/t/tarnation_vista.txt
+++ b/forge-gui/res/cardsfolder/t/tarnation_vista.txt
@@ -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.
diff --git a/forge-gui/res/cardsfolder/w/wirewood_channeler.txt b/forge-gui/res/cardsfolder/w/wirewood_channeler.txt
index b45488bd2cf..3eb2c998631 100644
--- a/forge-gui/res/cardsfolder/w/wirewood_channeler.txt
+++ b/forge-gui/res/cardsfolder/w/wirewood_channeler.txt
@@ -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.