diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java index a0afe54be58..4ee9eae9879 100644 --- a/forge-ai/src/main/java/forge/ai/GameState.java +++ b/forge-ai/src/main/java/forge/ai/GameState.java @@ -436,6 +436,13 @@ public abstract class GameState { } } + if (!c.getUnlockedRooms().isEmpty()) { + for (CardStateName stateName : c.getUnlockedRooms()) { + newText.append("|UnlockedRoom:"); + newText.append(stateName.name()); + } + } + cardTexts.put(zoneType, newText.toString()); } @@ -1401,6 +1408,8 @@ public abstract class GameState { c.setGamePieceType(GamePieceType.TOKEN); } else if (info.startsWith("ClassLevel:")) { c.setClassLevel(Integer.parseInt(info.substring(info.indexOf(':') + 1))); + } else if (info.startsWith("UnlockedRoom:")) { + c.unlockRoom(c.getController(), CardStateName.smartValueOf(info.substring(info.indexOf(':') + 1))); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java index 22b754f3dc3..83bbd6b2cc7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -160,6 +160,9 @@ public class ChangeZoneAi extends SpellAbilityAi { return SpecialCardAi.MazesEnd.consider(aiPlayer, sa); } else if (aiLogic.equals("Pongify")) { return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic + } else if (aiLogic.equals("ReturnCastable")) { + return !sa.getHostCard().getExiledCards().isEmpty() + && ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false); } } if (sa.isHidden()) { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java index 10c50ac98e2..721c183f635 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java @@ -1,25 +1,12 @@ package forge.ai.ability; -import java.util.Collections; -import java.util.List; -import java.util.Map; - import com.google.common.base.Predicates; import com.google.common.collect.Iterables; - -import forge.ai.AiAttackController; -import forge.ai.ComputerUtilAbility; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCombat; -import forge.ai.SpellAbilityAi; +import com.google.common.collect.Lists; +import forge.ai.*; import forge.game.Game; -import forge.game.card.Card; -import forge.game.card.CardCollection; -import forge.game.card.CardCollectionView; -import forge.game.card.CardLists; -import forge.game.card.CardPredicates; +import forge.game.card.*; import forge.game.card.CardPredicates.Presets; -import forge.game.card.CounterEnumType; import forge.game.combat.Combat; import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; @@ -30,6 +17,10 @@ import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import forge.util.Aggregates; +import java.util.Collections; +import java.util.List; +import java.util.Map; + public class ChooseCardAi extends SpellAbilityAi { /** @@ -58,11 +49,15 @@ public class ChooseCardAi extends SpellAbilityAi { protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { final Card host = sa.getHostCard(); final Game game = ai.getGame(); - ZoneType choiceZone = ZoneType.Battlefield; + + List choiceZone; if (sa.hasParam("ChoiceZone")) { - choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone")); + choiceZone = ZoneType.listValueOf(sa.getParam("ChoiceZone")); + } else { + choiceZone = Lists.newArrayList(ZoneType.Battlefield); } CardCollectionView choices = game.getCardsIn(choiceZone); + if (sa.hasParam("Choices")) { choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host, sa); } @@ -129,6 +124,13 @@ public class ChooseCardAi extends SpellAbilityAi { ownChoices = CardLists.filter(choices, CardPredicates.isControlledByAnyOf(ai.getAllies())); } return !ownChoices.isEmpty(); + } else if (aiLogic.equals("GoodCreature")) { + for (Card choice : choices) { + if (choice.isCreature() && ComputerUtilCard.evaluateCreature(choice) >= 250) { + return true; + } + } + return false; } return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java index 453f45ac6f5..9de4d7302fd 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java @@ -81,6 +81,13 @@ public class DrawAi extends SpellAbilityAi { if (!canLoot(ai, sa)) { return false; } + + if (ComputerUtilCost.isSacrificeSelfCost(sa.getPayCosts())) { + // Canopy lands and other cards that sacrifice themselves to draw cards + return ai.getCardsIn(ZoneType.Hand).isEmpty() + || (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5); // TODO: make this configurable in the AI profile + } + return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java index 1185df31edd..0b510d251e8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java @@ -42,6 +42,9 @@ public class LifeGainAi extends SpellAbilityAi { if (!lifeCritical) { // return super.willPayCosts(ai, sa, cost, source); + if ("CriticalOnly".equals(sa.getParam("AILogic"))) { + return false; + } if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa, false)) { return false; } diff --git a/forge-gui/res/cardsfolder/e/eladamri_korvecdal.txt b/forge-gui/res/cardsfolder/e/eladamri_korvecdal.txt index 2138799ecb0..fee1da6d5c8 100644 --- a/forge-gui/res/cardsfolder/e/eladamri_korvecdal.txt +++ b/forge-gui/res/cardsfolder/e/eladamri_korvecdal.txt @@ -4,7 +4,7 @@ Types:Legendary Creature Elf Warrior PT:3/3 S:Mode$ Continuous | Affected$ Card.TopLibrary+YouCtrl | AffectedZone$ Library | MayLookAt$ You | Description$ You may look at the top card of your library any time. S:Mode$ Continuous | Affected$ Creature.TopLibrary+YouCtrl+nonLand | AffectedZone$ Library | MayPlay$ True | Description$ You may cast creature spells from the top of your library. -A:AB$ ChooseCard | Cost$ G T tapXType<2/Creature> | ChoiceZone$ Hand,Library | PlayerTurn$ True | Reveal$ True | Choices$ Card.TopLibrary+YouOwn,Card.YouOwn+inZoneHand | SubAbility$ DBChangeZone | ChoiceTitle$ Reveal a card from your hand or the top of your library | SpellDescription$ Reveal a card from your hand or the top of your library. If you reveal a creature card this way, put it onto the battlefield. Activate only during your turn. +A:AB$ ChooseCard | Cost$ G T tapXType<2/Creature> | ChoiceZone$ Hand,Library | PlayerTurn$ True | Reveal$ True | Choices$ Card.TopLibrary+YouOwn,Card.YouOwn+inZoneHand | SubAbility$ DBChangeZone | ChoiceTitle$ Reveal a card from your hand or the top of your library | AILogic$ GoodCreature | SpellDescription$ Reveal a card from your hand or the top of your library. If you reveal a creature card this way, put it onto the battlefield. Activate only during your turn. SVar:DBChangeZone:DB$ ChangeZone | Defined$ ChosenCard | Origin$ Library,Hand | ConditionDefined$ ChosenCard | ConditionPresent$ Creature | Destination$ Battlefield | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearChosenCard$ True Oracle:You may look at the top of your library at any time.\nYou may cast creature spells from the top of your library.\n{G}, {T}, Tap two untapped creatures you control: Reveal a card from your hand or the top of your library. If you reveal a creature card this way, put it onto the battlefield. Activate only during your turn. diff --git a/forge-gui/res/cardsfolder/u/ugins_labyrinth.txt b/forge-gui/res/cardsfolder/u/ugins_labyrinth.txt index 9af78cf952d..155efa7b80a 100644 --- a/forge-gui/res/cardsfolder/u/ugins_labyrinth.txt +++ b/forge-gui/res/cardsfolder/u/ugins_labyrinth.txt @@ -4,7 +4,7 @@ Types:Land T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | OptionalDecider$ You | Execute$ TrigExile | TriggerDescription$ Imprint — When CARDNAME enters, you may exile a colorless card with mana value 7 or greater from your hand. SVar:TrigExile:DB$ ChangeZone | Origin$ Hand | Destination$ Exile | ChangeType$ Card.Colorless+cmcGE7 | ChangeNum$ 1 A:AB$ Mana | Cost$ T | Produced$ C | Amount$ X | SpellDescription$ Add {C}. If a card is exiled with CARDNAME, add {C}{C} instead. -A:AB$ ChangeZone | Cost$ T | Defined$ ExiledWith | Origin$ Exile | Destination$ Hand | SpellDescription$ Return the exiled card to its owner's hand. +A:AB$ ChangeZone | Cost$ T | Defined$ ExiledWith | Origin$ Exile | Destination$ Hand | AILogic$ ReturnCastable | SpellDescription$ Return the exiled card to its owner's hand. SVar:X:Count$Compare Y GE1.2.1 SVar:Y:Count$ValidExile Card.ExiledWithSource Oracle:Imprint — When Ugin's Labyrinth enters, you may exile a colorless card with mana value 7 or greater from your hand.\n{T}: Add {C}. If a card is exiled with Ugin's Labyrinth, add {C}{C} instead.\n{T}: Return the exiled card to its owner's hand. diff --git a/forge-gui/res/cardsfolder/z/zuran_orb.txt b/forge-gui/res/cardsfolder/z/zuran_orb.txt index 9be1520a4cf..8d40776dbc4 100644 --- a/forge-gui/res/cardsfolder/z/zuran_orb.txt +++ b/forge-gui/res/cardsfolder/z/zuran_orb.txt @@ -1,6 +1,6 @@ Name:Zuran Orb ManaCost:0 Types:Artifact -A:AB$ GainLife | Cost$ Sac<1/Land> | LifeAmount$ 2 | SpellDescription$ You gain 2 life. +A:AB$ GainLife | Cost$ Sac<1/Land> | LifeAmount$ 2 | AILogic$ CriticalOnly | SpellDescription$ You gain 2 life. SVar:NonStackingEffect:True Oracle:Sacrifice a land: You gain 2 life. diff --git a/forge-gui/res/puzzle/PS_DSK3.pzl b/forge-gui/res/puzzle/PS_DSK3.pzl new file mode 100644 index 00000000000..75f977e0c5c --- /dev/null +++ b/forge-gui/res/puzzle/PS_DSK3.pzl @@ -0,0 +1,17 @@ +[metadata] +Name:Possibility Storm - Duskmourn: House of Horror #03 +URL:https://i0.wp.com/www.possibilitystorm.com/wp-content/uploads/2024/10/latest-scaled.jpg?ssl=1 +Goal:Win +Turns:1 +Difficulty:Uncommon +Description:Win this turn. Smoky Lounge is unlocked, and you start the puzzle with its ability on the stack. (Misty Salon is locked.) Ensure your solution satisfies all possible blocking decisions. Good luck! +[state] +turn=1 +activeplayer=p0 +activephase=DRAW +activephaseadvance=MAIN1 +p0life=20 +p0hand=Painter's Studio // Defaced Gallery;Song of Totentanz;Ghostly Dancers;Dollmaker's Shop // Porcelain Gallery +p0battlefield=Marina Vendrell;T:everywhere;T:everywhere;T:everywhere;T:everywhere;T:everywhere;T:everywhere;Smoky Lounge // Misty Salon|UnlockedRoom:LeftSplit +p1life=12 +p1battlefield=Miasma Demon;Miasma Demon diff --git a/forge-installer/libs/install.xml b/forge-installer/libs/install.xml index e1aa18b1312..175b8d0e17f 100644 --- a/forge-installer/libs/install.xml +++ b/forge-installer/libs/install.xml @@ -68,14 +68,23 @@ + + + + + + + + + - + diff --git a/forge-installer/pom.xml b/forge-installer/pom.xml index 40604fff99b..f81742660c8 100644 --- a/forge-installer/pom.xml +++ b/forge-installer/pom.xml @@ -206,13 +206,16 @@ - - - + + + + + +