diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 8a8a4734bed..02f4d0fe1a0 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -716,7 +716,6 @@ public class ComputerUtil { CardCollection remaining = new CardCollection(cardlist); final CardCollection sacrificed = new CardCollection(); final Card host = source.getHostCard(); - final boolean considerSacLogic = "ConsiderSac".equals(source.getParam("AILogic")); final int considerSacThreshold = getAIPreferenceParameter(host, "CreatureEvalThreshold"); if ("OpponentOnly".equals(source.getParam("AILogic"))) { @@ -732,14 +731,14 @@ public class ComputerUtil { if (!ai.canLoseLife() || ai.cantLose()) { return sacrificed; // sacrifice none } - } else if (!considerSacLogic) { + } else { return sacrificed; // sacrifice none } } boolean exceptSelf = "ExceptSelf".equals(source.getParam("AILogic")); boolean removedSelf = false; - if (isOptional && source.hasParam("Devour") || source.hasParam("Exploit") || considerSacLogic) { + if (isOptional && source.hasParam("Devour") || source.hasParam("Exploit")) { if (source.hasParam("Exploit")) { for (Trigger t : host.getTriggers()) { if (t.getMode() == TriggerType.Exploited) { @@ -761,17 +760,21 @@ public class ComputerUtil { public boolean apply(final Card c) { int sacThreshold = 190; - if ("HeartPiercer".equals(source.getParam("SacrificeParam"))) { - if (c.getNetPower() == 0) { + String logic = source.getParamOrDefault("AILogic", ""); + if (logic.startsWith("SacForDamage")) { + if (c.getNetPower() <= 0) { return false; } else if (c.getNetPower() >= ai.getOpponentsSmallestLifeTotal()) { return true; + } else if (logic.endsWith(".GiantX2") && c.getType().hasCreatureType("Giant") + && c.getNetPower() * 2 >= ai.getOpponentsSmallestLifeTotal()) { + return true; // TODO: generalize this for any type and actually make the AI prefer giants? } } if ("DesecrationDemon".equals(source.getParam("AILogic"))) { sacThreshold = SpecialCardAi.DesecrationDemon.getSacThreshold(); - } else if (considerSacLogic && considerSacThreshold != -1) { + } else if (considerSacThreshold != -1) { sacThreshold = considerSacThreshold; } diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java index 81fa2071d21..7991dbd62ab 100644 --- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java @@ -91,7 +91,7 @@ public enum SpellApiToAi { .put(ApiType.GenericChoice, ChooseGenericEffectAi.class) .put(ApiType.Goad, GoadAi.class) .put(ApiType.Haunt, HauntAi.class) - .put(ApiType.ImmediateTrigger, AlwaysPlayAi.class) + .put(ApiType.ImmediateTrigger, DelayedTriggerAi.class) .put(ApiType.Investigate, InvestigateAi.class) .put(ApiType.LoseLife, LifeLoseAi.class) .put(ApiType.LosesGame, GameLossAi.class) diff --git a/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java b/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java new file mode 100644 index 00000000000..2619018d4bc --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java @@ -0,0 +1,69 @@ +package forge.ai.ability; + +import forge.ai.*; +import forge.game.ability.AbilityFactory; +import forge.game.player.Player; +import forge.game.spellability.AbilitySub; +import forge.game.spellability.SpellAbility; + +public class ImmediateTriggerAi extends SpellAbilityAi { + // TODO: this class is largely reused from DelayedTriggerAi, consider updating + + @Override + public boolean chkAIDrawback(SpellAbility sa, Player ai) { + String logic = sa.getParamOrDefault("AILogic", ""); + if (logic.equals("Always")) { + return true; + } + + SpellAbility trigsa = null; + if (sa.hasAdditionalAbility("Execute")) { + trigsa = sa.getAdditionalAbility("Execute"); + } else { + trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute")); + } + trigsa.setActivatingPlayer(ai); + + if (trigsa instanceof AbilitySub) { + return SpellApiToAi.Converter.get(trigsa.getApi()).chkDrawbackWithSubs(ai, (AbilitySub)trigsa); + } else { + return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); + } + } + + @Override + protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + SpellAbility trigsa = null; + if (sa.hasAdditionalAbility("Execute")) { + trigsa = sa.getAdditionalAbility("Execute"); + } else { + trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute")); + } + AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); + trigsa.setActivatingPlayer(ai); + + if (!sa.hasParam("OptionalDecider")) { + return aic.doTrigger(trigsa, true); + } else { + return aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You")); + } + } + + @Override + protected boolean canPlayAI(Player ai, SpellAbility sa) { + String logic = sa.getParamOrDefault("AILogic", ""); + if (logic.equals("Always")) { + return true; + } + + SpellAbility trigsa = null; + if (sa.hasAdditionalAbility("Execute")) { + trigsa = sa.getAdditionalAbility("Execute"); + } else { + trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute")); + } + trigsa.setActivatingPlayer(ai); + return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); + } + +} diff --git a/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java b/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java index 28ba3cbdda8..80eb4a62da5 100644 --- a/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java +++ b/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java @@ -17,7 +17,6 @@ import java.util.Map.Entry; import java.util.Set; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Lists; import com.google.common.collect.Maps; // Wrapper ability that checks the requirements again just before @@ -487,18 +486,6 @@ public class WrappedAbility extends Ability { return; } - // Check timestamps of triggered objects - final List original = Lists.newArrayList(sa.getTriggerRemembered()); - for (Object o : original) { - if (o instanceof Card) { - Card card = (Card) o; - Card current = game.getCardState(card); - if (current.getTimestamp() != card.getTimestamp()) { - // TODO: figure out if NoTimestampCheck should be the default for ChangesZone triggers - sa.getTriggerRemembered().remove(o); - } - } - } final Map triggerMap = AbilityKey.newMap(sa.getTriggeringObjects()); for (Entry ev : triggerMap.entrySet()) { if (ev.getValue() instanceof Card) { diff --git a/forge-gui/res/cardsfolder/h/heart_piercer_manticore.txt b/forge-gui/res/cardsfolder/h/heart_piercer_manticore.txt index 68b9bfc0e31..219317eb072 100644 --- a/forge-gui/res/cardsfolder/h/heart_piercer_manticore.txt +++ b/forge-gui/res/cardsfolder/h/heart_piercer_manticore.txt @@ -2,13 +2,11 @@ Name:Heart-Piercer Manticore ManaCost:2 R R Types:Creature Manticore PT:4/3 -T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | TriggerZones$ Battlefield | Execute$ TrigSacrifice | TriggerDescription$ When CARDNAME enters the battlefield, you may sacrifice another creature. When you do, CARDNAME deals damage equal to that creature's power to target creature or player. -SVar:TrigSacrifice:DB$ Sacrifice | Optional$ True | SacValid$ Creature.Other | SacMessage$ another Creature | Amount$ 1 | AILogic$ ConsiderSac | RememberSacrificed$ True | SubAbility$ DBTrigger -SVar:DBTrigger:DB$ ImmediateTrigger | Execute$ TrigDamage | RememberObjects$ RememberedCard | ConditionDefined$ Remembered | ConditionPresent$ Card | SubAbility$ DBCleanup | TriggerDescription$ When you do, CARDNAME deals damage equal to that creature's power to any target. +T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | TriggerZones$ Battlefield | Execute$ DBTrigger | TriggerDescription$ When CARDNAME enters the battlefield, you may sacrifice another creature. When you do, CARDNAME deals damage equal to that creature's power to target creature or player. +SVar:DBTrigger:AB$ ImmediateTrigger | Cost$ Sac<1/Creature.Other/another creature> | Execute$ TrigDamage | AILogic$ SacForDamage | RememberObjects$ Sacrificed | TriggerDescription$ When you do, CARDNAME deals damage equal to that creature's power to any target. SVar:TrigDamage:DB$ DealDamage | ValidTgts$ Creature,Player,Planeswalker | TgtPrompt$ Select any target | NumDmg$ XPower | References$ XPower -SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True K:Embalm:5 R SVar:XPower:TriggerRemembered$CardPower SVar:AIPreferenceParams:CreatureEvalThreshold$ 200 -SVar:DeckHas:Ability$Token +DeckHas:Ability$Token & Ability$Sacrifice Oracle:When Heart-Piercer Manticore enters the battlefield, you may sacrifice another creature. When you do, Heart-Piercer Manticore deals damage equal to that creature's power to any target.\nEmbalm {5} {R} ({5} {R}, Exile this card from your graveyard: Create a token that's a copy of it, except it's a white Zombie Manticore with no mana cost. Embalm only as a sorcery.) diff --git a/forge-gui/res/cardsfolder/upcoming/surtland_flinger.txt b/forge-gui/res/cardsfolder/upcoming/surtland_flinger.txt new file mode 100644 index 00000000000..923ece0cbed --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/surtland_flinger.txt @@ -0,0 +1,15 @@ +Name:Surtland Flinger +ManaCost:3 R R +Types:Creature Giant Berserker +PT:4/6 +T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigImmediate | TriggerDescription$ Whenever CARDNAME attacks, you may sacrifice another creature. When you do, CARDNAME deals damage equal to the sacrificed creature's power to any target. If the sacrificed creature was a Giant, CARDNAME deals twice that much damage instead. +SVar:TrigImmediate:AB$ ImmediateTrigger | Cost$ Sac<1/Creature.Other/another creature> | RememberObjects$ Sacrificed | Execute$ TrigDamage | AILogic$ SacForDamage.GiantX2 | TriggerDescription$ When you do, CARDNAME deals damage equal to the sacrificed creature's power to any target. If the sacrificed creature was a Giant, CARDNAME deals twice that much damage instead. +SVar:TrigDamage:DB$ DealDamage | ConditionDefined$ DelayTriggerRememberedLKI | ConditionPresent$ Giant | ConditionCompare$ EQ0 | ValidTgts$ Creature,Player,Planeswalker | TgtPrompt$ Select any target | NumDmg$ X | References$ X | SubAbility$ DBGiantDamage +SVar:DBGiantDamage:DB$ DealDamage | ConditionDefined$ DelayTriggerRememberedLKI | ConditionPresent$ Giant | ConditionCompare$ GE1 | Defined$ Targeted | NumDmg$ Y | References$ Y +SVar:X:TriggerRemembered$CardPower +SVar:Y:TriggerRemembered$CardPower/Times.2 +SVar:AIPreferenceParams:CreatureEvalThreshold$ 200 +SVar:HasAttackEffect:TRUE +DeckHas:Ability$Sacrifice +DeckHints:Type$Giant +Oracle:Whenever Surtland Flinger attacks, you may sacrifice another creature. When you do, Surtland Flinger deals damage equal to the sacrificed creature’s power to any target. If the sacrificed creature was a Giant, Surtland Flinger deals twice that much damage instead.