From 31f8da56877d87d63c42b8dd69439709e72921d7 Mon Sep 17 00:00:00 2001 From: tool4ever Date: Wed, 1 Oct 2025 16:54:46 +0200 Subject: [PATCH] Some fixes (#8816) --- .../src/main/java/forge/ai/AiController.java | 44 ++++--------------- .../java/forge/ai/PlayerControllerAi.java | 6 ++- .../main/java/forge/ai/ability/CloneAi.java | 4 ++ .../java/forge/ai/ability/CountersPutAi.java | 37 +++++----------- .../main/java/forge/ai/ability/EffectAi.java | 2 +- .../main/java/forge/ai/ability/FightAi.java | 2 +- .../main/java/forge/ai/ability/PumpAi.java | 2 +- .../game/ability/effects/EffectEffect.java | 10 ++--- .../java/forge/game/card/CardFactoryUtil.java | 2 +- .../res/cardsfolder/s/samite_ministration.txt | 8 ++-- .../upcoming/atreus_impulsive_son.txt | 3 +- 11 files changed, 43 insertions(+), 77 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 00beb693357..c9d698c7582 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -887,27 +887,8 @@ public class AiController { private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) { final Card host = sa.getHostCard(); - // Check a predefined condition - if (sa.hasParam("AICheckSVar")) { - final String svarToCheck = sa.getParam("AICheckSVar"); - String comparator = "GE"; - int compareTo = 1; - - if (sa.hasParam("AISVarCompare")) { - final String fullCmp = sa.getParam("AISVarCompare"); - comparator = fullCmp.substring(0, 2); - final String strCmpTo = fullCmp.substring(2); - try { - compareTo = Integer.parseInt(strCmpTo); - } catch (final Exception ignored) { - compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa); - } - } - - int left = AbilityUtils.calculateAmount(host, svarToCheck, sa); - if (!Expressions.compare(left, comparator, compareTo)) { - return AiPlayDecision.AnotherTime; - } + if (sa.hasParam("AICheckSVar") && !aiShouldRun(sa, sa, host, null)) { + return AiPlayDecision.AnotherTime; } // this is the "heaviest" check, which also sets up targets, defines X, etc. @@ -1817,14 +1798,9 @@ public class AiController { * @param sa the sa * @return true, if successful */ - public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa, GameEntity affected) { - Card hostCard = effect.getHostCard(); - if (hostCard.hasAlternateState()) { - hostCard = game.getCardState(hostCard); - } - + public final boolean aiShouldRun(final CardTraitBase effect, final SpellAbility sa, final Card host, final GameEntity affected) { if (effect.hasParam("AILogic") && effect.getParam("AILogic").equalsIgnoreCase("ProtectFriendly")) { - final Player controller = hostCard.getController(); + final Player controller = host.getController(); if (affected instanceof Player) { return !((Player) affected).isOpponentOf(controller); } @@ -1833,7 +1809,6 @@ public class AiController { } } if (effect.hasParam("AICheckSVar")) { - System.out.println("aiShouldRun?" + sa); final String svarToCheck = effect.getParam("AICheckSVar"); String comparator = "GE"; int compareTo = 1; @@ -1846,9 +1821,9 @@ public class AiController { compareTo = Integer.parseInt(strCmpTo); } catch (final Exception ignored) { if (sa == null) { - compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), effect); + compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), effect); } else { - compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), sa); + compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa); } } } @@ -1856,13 +1831,12 @@ public class AiController { int left = 0; if (sa == null) { - left = AbilityUtils.calculateAmount(hostCard, svarToCheck, effect); + left = AbilityUtils.calculateAmount(host, svarToCheck, effect); } else { - left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa); + left = AbilityUtils.calculateAmount(host, svarToCheck, sa); } - System.out.println("aiShouldRun?" + left + comparator + compareTo); return Expressions.compare(left, comparator, compareTo); - } else if (effect.hasParam("AICheckDredge")) { + } else if (effect.isKeyword(Keyword.DREDGE)) { return player.getCardsIn(ZoneType.Library).size() > 8 || player.isCardInPlay("Laboratory Maniac"); } else return sa != null && doTrigger(sa, false); } diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 1b94140f717..81bc75db880 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -460,7 +460,11 @@ public class PlayerControllerAi extends PlayerController { @Override public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) { - return brains.aiShouldRun(replacementEffect, effectSA, affected); + Card host = replacementEffect.getHostCard(); + if (host.hasAlternateState()) { + host = host.getGame().getCardState(host); + } + return brains.aiShouldRun(replacementEffect, effectSA, host, affected); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java index 942bde1491f..b7199681f47 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java @@ -96,6 +96,10 @@ public class CloneAi extends SpellAbilityAi { if (sa.usesTargeting()) { chance = cloneTgtAI(sa); } else { + if (sa.isReplacementAbility() && host.isCloned()) { + // prevent StackOverflow from infinite loop copying another ETB RE + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } if (sa.hasParam("Choices")) { CardCollectionView choices = CardLists.getValidCards(host.getGame().getCardsIn(ZoneType.Battlefield), sa.getParam("Choices"), host.getController(), host, sa); diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index e8c18354697..77dfc9c9443 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -92,9 +92,8 @@ public class CountersPutAi extends CountersAi { return false; } return chance > MyRandom.getRandom().nextFloat(); - } else { - return false; } + return false; } if (sa.isKeyword(Keyword.LEVEL_UP)) { @@ -124,7 +123,6 @@ public class CountersPutAi extends CountersAi { final Cost abCost = sa.getPayCosts(); final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); - CardCollection list; Card choice = null; final String amountStr = sa.getParamOrDefault("CounterNum", "1"); final boolean divided = sa.isDividedAsYouChoose(); @@ -292,10 +290,8 @@ public class CountersPutAi extends CountersAi { if (willActivate) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("ChargeToBestCMC")) { return doChargeToCMCLogic(ai, sa); } else if (logic.equals("ChargeToBestOppControlledCMC")) { @@ -348,7 +344,7 @@ public class CountersPutAi extends CountersAi { if (type.equals("P1P1")) { nPump = amount; } - return FightAi.canFightAi(ai, sa, nPump, nPump); + return FightAi.canFight(ai, sa, nPump, nPump); } if (amountStr.equals("X")) { @@ -451,6 +447,7 @@ public class CountersPutAi extends CountersAi { sa.resetTargets(); + CardCollection list; if (sa.isCurse()) { list = ai.getOpponents().getCardsIn(ZoneType.Battlefield); } else { @@ -746,7 +743,7 @@ public class CountersPutAi extends CountersAi { protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) { final SpellAbility root = sa.getRootAbility(); final Card source = sa.getHostCard(); - final String aiLogic = sa.getParamOrDefault("AILogic", ""); + final String aiLogic = sa.getParam("AILogic"); final String amountStr = sa.getParamOrDefault("CounterNum", "1"); final boolean divided = sa.isDividedAsYouChoose(); final int amount = AbilityUtils.calculateAmount(source, amountStr, sa); @@ -765,14 +762,10 @@ public class CountersPutAi extends CountersAi { } if ("ChargeToBestCMC".equals(aiLogic)) { - AiAbilityDecision decision = doChargeToCMCLogic(ai, sa); - if (decision.willingToPlay()) { - return decision; - } if (mandatory) { return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); } - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return doChargeToCMCLogic(ai, sa); } if (!sa.usesTargeting()) { @@ -796,7 +789,6 @@ public class CountersPutAi extends CountersAi { // things like Powder Keg, which are way too complex for the AI } } else if (sa.getTargetRestrictions().canOnlyTgtOpponent() && !sa.getTargetRestrictions().canTgtCreature()) { - // can only target opponent PlayerCollection playerList = new PlayerCollection(IterableUtil.filter( sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class)); @@ -811,13 +803,12 @@ public class CountersPutAi extends CountersAi { sa.getTargets().add(choice); } } else { - String logic = sa.getParam("AILogic"); - if ("Fight".equals(logic) || "PowerDmg".equals(logic)) { + if ("Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic)) { int nPump = 0; if (type.equals("P1P1")) { nPump = amount; } - AiAbilityDecision decision = FightAi.canFightAi(ai, sa, nPump, nPump); + AiAbilityDecision decision = FightAi.canFight(ai, sa, nPump, nPump); if (decision.willingToPlay()) { return decision; } @@ -838,7 +829,6 @@ public class CountersPutAi extends CountersAi { while (sa.canAddMoreTarget()) { if (mandatory) { - // When things are mandatory, gotta handle a little differently if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) { return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } @@ -863,7 +853,7 @@ public class CountersPutAi extends CountersAi { return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi); } - Card choice = null; + Card choice; // Choose targets here: if (sa.isCurse()) { @@ -889,10 +879,10 @@ public class CountersPutAi extends CountersAi { choice = Aggregates.random(list); } if (choice != null && divided) { - int alloc = Math.max(amount / totalTargets, 1); if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) { sa.addDividedAllocation(choice, left); } else { + int alloc = Math.max(amount / totalTargets, 1); sa.addDividedAllocation(choice, alloc); left -= alloc; } @@ -982,9 +972,7 @@ public class CountersPutAi extends CountersAi { final String amountStr = sa.getParamOrDefault("CounterNum", "1"); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); - final boolean isCurse = sa.isCurse(); - - if (isCurse) { + if (sa.isCurse()) { final CardCollection opponents = CardLists.filterControlledBy(options, ai.getOpponents()); if (!opponents.isEmpty()) { @@ -1210,9 +1198,8 @@ public class CountersPutAi extends CountersAi { } if (numCtrs < optimalCMC) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } private AiAbilityDecision doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) { diff --git a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java index 746a5b7623f..8e7b0d073bc 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java @@ -270,7 +270,7 @@ public class EffectAi extends SpellAbilityAi { } return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("Fight")) { - return FightAi.canFightAi(ai, sa, 0,0); + return FightAi.canFight(ai, sa, 0,0); } else if (logic.equals("Pump")) { sa.resetTargets(); List options = CardUtil.getValidCardsToTarget(sa); diff --git a/forge-ai/src/main/java/forge/ai/ability/FightAi.java b/forge-ai/src/main/java/forge/ai/ability/FightAi.java index 083aa8c155b..ff15bb89907 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FightAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FightAi.java @@ -177,7 +177,7 @@ public class FightAi extends SpellAbilityAi { * @param power bonus to power * @return true if fight effect should be played, false otherwise */ - public static AiAbilityDecision canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) { + public static AiAbilityDecision canFight(final Player ai, final SpellAbility sa, int power, int toughness) { final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); AbilitySub tgtFight = sa.getSubAbility(); diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java index 3c0cec2634d..bc7c4f8431e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java @@ -453,7 +453,7 @@ public class PumpAi extends PumpAiBase { } if (isFight) { - return FightAi.canFightAi(ai, sa, attack, defense).willingToPlay(); + return FightAi.canFight(ai, sa, attack, defense).willingToPlay(); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java b/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java index a7ca54bd0e2..954c833b7ab 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java @@ -269,22 +269,22 @@ public class EffectEffect extends SpellAbilityEffect { } } - // Set Chosen Color(s) if (hostCard.hasChosenColor()) { eff.setChosenColors(Lists.newArrayList(hostCard.getChosenColors())); } - // Set Chosen Cards if (hostCard.hasChosenCard()) { eff.setChosenCards(hostCard.getChosenCards()); } - // Set Chosen Player if (hostCard.hasChosenPlayer()) { eff.setChosenPlayer(hostCard.getChosenPlayer()); } - // Set Chosen Type + if (hostCard.getChosenDirection() != null) { + eff.setChosenDirection(hostCard.getChosenDirection()); + } + if (hostCard.hasChosenType()) { eff.setChosenType(hostCard.getChosenType()); } @@ -292,12 +292,10 @@ public class EffectEffect extends SpellAbilityEffect { eff.setChosenType2(hostCard.getChosenType2()); } - // Set Chosen name if (hostCard.hasNamedCard()) { eff.setNamedCards(Lists.newArrayList(hostCard.getNamedCards())); } - // chosen number if (sa.hasParam("SetChosenNumber")) { eff.setChosenNumber(AbilityUtils.calculateAmount(hostCard, sa.getParam("SetChosenNumber"), sa)); } else if (hostCard.hasChosenNumber()) { diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 98a32ff3b00..f5a3c133177 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -2238,7 +2238,7 @@ public class CardFactoryUtil { final String actualRep = "Event$ Draw | ActiveZones$ Graveyard | ValidPlayer$ You | " + "Secondary$ True | Optional$ True | CheckSVar$ " + "DredgeCheckLib | SVarCompare$ GE" + dredgeAmount - + " | AICheckDredge$ True | Description$ CARDNAME - Dredge " + dredgeAmount; + + " | Description$ CARDNAME - Dredge " + dredgeAmount; final String abString = "DB$ Mill | Defined$ You | NumCards$ " + dredgeAmount; diff --git a/forge-gui/res/cardsfolder/s/samite_ministration.txt b/forge-gui/res/cardsfolder/s/samite_ministration.txt index dbd342c8b59..367118617af 100644 --- a/forge-gui/res/cardsfolder/s/samite_ministration.txt +++ b/forge-gui/res/cardsfolder/s/samite_ministration.txt @@ -3,11 +3,9 @@ ManaCost:1 W Types:Instant A:SP$ ChooseSource | Choices$ Card,Emblem | AILogic$ NeedsPrevention | SubAbility$ DBEffect | StackDescription$ SpellDescription | SpellDescription$ Prevent all damage that would be dealt to you this turn by a source of your choice. Whenever damage from a black or red source is prevented this way this turn, you gain that much life. SVar:DBEffect:DB$ Effect | ReplacementEffects$ RepDmg | ConditionDefined$ ChosenCard | ConditionPresent$ Card,Emblem | ConditionCompare$ GE1 -SVar:RepDmg:Event$ DamageDone | ValidTarget$ You | ValidSource$ Card.ChosenCardStrict,Emblem.ChosenCard | ReplaceWith$ DBStoreSVar | PreventionEffect$ True | Description$ Prevent all damage that would be dealt to you this turn by a source of your choice. Whenever damage from a black or red source is prevented this way this turn, you gain that much life. -SVar:DBStoreSVar:DB$ StoreSVar | SVar$ Z | Type$ Calculate | Expression$ X | SubAbility$ DBTrigger +SVar:RepDmg:Event$ DamageDone | ValidTarget$ You | ValidSource$ Card.ChosenCardStrict,Emblem.ChosenCard | ReplaceWith$ DBTrigger | PreventionEffect$ True | Description$ Prevent all damage that would be dealt to you this turn by a source of your choice. Whenever damage from a black or red source is prevented this way this turn, you gain that much life. SVar:DBTrigger:DB$ ImmediateTrigger | Execute$ GainLifeYou | ConditionCheckSVar$ Y | ConditionSVarCompare$ GE1 | TriggerDescription$ Whenever damage from a black or red source is prevented this way this turn, you gain that much life. -SVar:GainLifeYou:DB$ GainLife | Defined$ You | LifeAmount$ Z -SVar:X:ReplaceCount$DamageAmount +SVar:GainLifeYou:DB$ GainLife | Defined$ You | LifeAmount$ X +SVar:X:Spawner>ReplaceCount$DamageAmount SVar:Y:ReplacedSource$Valid Card.BlackSource,Card.RedSource -SVar:Z:Number$0 Oracle:Prevent all damage that would be dealt to you this turn by a source of your choice. Whenever damage from a black or red source is prevented this way this turn, you gain that much life. diff --git a/forge-gui/res/cardsfolder/upcoming/atreus_impulsive_son.txt b/forge-gui/res/cardsfolder/upcoming/atreus_impulsive_son.txt index b6fc41bbc8d..c6a78a975db 100644 --- a/forge-gui/res/cardsfolder/upcoming/atreus_impulsive_son.txt +++ b/forge-gui/res/cardsfolder/upcoming/atreus_impulsive_son.txt @@ -4,7 +4,8 @@ Types:Legendary Creature God Archer PT:2/4 K:Reach K:Partner - Father & Son -A:AB$ Draw | Cost$ 3 T | NumCards$ X | SubAbility$ DBDamage | SpellDescription$ Draw a card for each experience counter you have, then discard a card. +A:AB$ Draw | Cost$ 3 T | NumCards$ X | SubAbility$ DBDiscard | SpellDescription$ Draw a card for each experience counter you have, then discard a card. +SVar:DBDiscard:DB$ Discard | Mode$ YouChoose | SubAbility$ DBDamage SVar:DBDamage:DB$ DealDamage | NumDmg$ 2 | Defined$ Opponent | SpellDescription$ CARDNAME deals 2 damage to each opponent. SVar:X:Count$YourCountersExperience Oracle:Reach\n{3}, {T}: Draw a card for each experience counter you have, then discard a card. Atreus, Impulsive Son deals 2 damage to each opponent.\nPartner-Father & son