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.