diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 26c2d1449bb..edcbb5004b0 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -355,6 +355,31 @@ public class AiController { return false; } } + + for (final Trigger tr : card.getTriggers()) { + if (!card.hasStartOfKeyword("Saga") && !card.hasStartOfKeyword("Read ahead")) { + break; + } + + if (tr.getMode() != TriggerType.CounterAdded) { + continue; + } + + SpellAbility exSA = tr.ensureAbility().copy(activator); + + if (api != null && exSA.getApi() == api) { + rightapi = true; + } + + if (exSA instanceof AbilitySub && !doTrigger(exSA, false)) { + // AI would not run this chapter if given the chance + // TODO eventually we'll want to consider playing it anyway, especially if Read ahead would still allow an immediate benefit + return false; + } + + break; + } + if (api != null && !rightapi) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java index 9e5f95d5e6e..65a7571eafb 100644 --- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java @@ -177,7 +177,7 @@ public class AiCostDecision extends CostDecisionMakerBase { // TODO Determine exile from same zone for AI return null; } else { - CardCollectionView chosen = ComputerUtil.chooseExileFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c, ability); + CardCollectionView chosen = ComputerUtil.chooseExileFrom(player, cost, source, c, ability); return null == chosen ? null : PaymentDecision.card(chosen); } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 74806b1f430..91bf2bb9334 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -66,6 +66,7 @@ import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.cost.Cost; import forge.game.cost.CostDiscard; +import forge.game.cost.CostExile; import forge.game.cost.CostPart; import forge.game.cost.CostPayment; import forge.game.cost.CostPutCounter; @@ -641,9 +642,14 @@ public class ComputerUtil { return sacList; } - public static CardCollection chooseExileFrom(final Player ai, final ZoneType zone, final String type, final Card activate, - final Card target, final int amount, SpellAbility sa) { - CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(zone), type.split(";"), activate.getController(), activate, sa); + public static CardCollection chooseExileFrom(final Player ai, CostExile cost, final Card activate, final int amount, SpellAbility sa) { + CardCollection typeList; + if (cost.zoneRestriction != 1) { + typeList = new CardCollection(ai.getGame().getCardsIn(cost.from)); + } else { + typeList = new CardCollection(ai.getCardsIn(cost.from)); + } + typeList = CardLists.getValidCards(typeList, cost.getType().split(";"), activate.getController(), activate, sa); // don't exile the card we're pumping typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, sa, ai); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index e76cc329aee..ffbc38f8640 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -2410,7 +2410,6 @@ public class ComputerUtilCombat { // (currently looks for the creature with maximum raw power since that's what the AI usually judges by when // deciding whether the creature is worth blocking). // If the creature doesn't change into anything, returns the original creature. - if (attacker == null) { return null; } Card attackerAfterTrigs = attacker; // Test for some special triggers that can change the creature in combat diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java index 412d70ef807..06a4b539863 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -1412,7 +1412,7 @@ public class AttachAi extends SpellAbilityAi { } // avoid randomly moving the equipment back and forth between several creatures in one turn - if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN) && !mandatory) { + if (AiCardMemory.isRememberedCard(aiPlayer, attachSource, AiCardMemory.MemorySet.ATTACHED_THIS_TURN) && !mandatory) { return null; } @@ -1423,7 +1423,7 @@ public class AttachAi extends SpellAbilityAi { } } - AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN); + AiCardMemory.rememberCard(aiPlayer, attachSource, AiCardMemory.MemorySet.ATTACHED_THIS_TURN); if (c == null && mandatory) { CardLists.shuffle(list); @@ -1674,12 +1674,6 @@ public class AttachAi extends SpellAbilityAi { if (c == null) { return false; } - if (sa.getHostCard() == null) { - // FIXME: Not sure what should the resolution be if a SpellAbility has no host card. This should - // not happen normally. Possibly remove this block altogether? (if it's an impossible condition). - System.out.println("AttachAi: isUsefulAttachAction unexpectedly called with SpellAbility with no host card. Assuming it's a determined useful action."); - return true; - } // useless to equip a creature that can't attack or block. return !sa.getHostCard().isEquipment() || !ComputerUtilCard.isUselessCreature(ai, c); 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 e181d18a202..30534590125 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java @@ -69,7 +69,6 @@ public class CloneAi extends SpellAbilityAi { bFlag = true; } } - } if (!bFlag) { // All of the defined stuff is cloned, not very useful 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 5a195371003..495d930cd89 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -1221,4 +1221,12 @@ public class CountersPutAi extends CountersAi { return false; } + @Override + public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map params) { + if (sa.hasParam("ReadAhead")) { + return 1; + } + return max; + } + } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersNoteEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersNoteEffect.java index 506b2557486..e08ac8d5a1f 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersNoteEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersNoteEffect.java @@ -2,8 +2,6 @@ package forge.game.ability.effects; import java.util.Map.Entry; -import forge.game.Game; -import forge.game.GameEntityCounterTable; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CounterType; @@ -21,19 +19,16 @@ public class CountersNoteEffect extends SpellAbilityEffect { @Override public void resolve(SpellAbility sa) { Card source = sa.getHostCard(); - final Game game = source.getGame(); Player p = sa.getActivatingPlayer(); String mode = sa.getParamOrDefault("Mode", "Load"); - GameEntityCounterTable table = new GameEntityCounterTable(); for (Card c : getDefinedCardsOrTargeted(sa)) { if (mode.equals(MODE_STORE)) { noteCounters(c, source); } else if (mode.equals(MODE_LOAD)) { - loadCounters(c, source, p, sa, table); + loadCounters(c, source, p, sa); } } - table.replaceCounterEffect(game, sa, false); } public static void noteCounters(Card notee, Card source) { @@ -44,13 +39,13 @@ public class CountersNoteEffect extends SpellAbilityEffect { } } - private void loadCounters(Card notee, Card source, final Player p, final SpellAbility sa, GameEntityCounterTable table) { + private void loadCounters(Card notee, Card source, final Player p, final SpellAbility sa) { for (Entry svar : source.getSVars().entrySet()) { String key = svar.getKey(); if (key.startsWith(NOTE_COUNTERS)) { - notee.addCounter( + notee.addEtbCounter( CounterType.getType(key.substring(NOTE_COUNTERS.length())), - Integer.parseInt(svar.getValue()), p, table); + Integer.parseInt(svar.getValue()), p); } // TODO Probably should "remove" the svars that were temporarily used } diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index df75ded991d..0fc3b9723b9 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -6374,7 +6374,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { continue; } - if (!params.get("Destination").equals(ZoneType.Battlefield.toString())) { + if (!ZoneType.Battlefield.toString().equals(params.get("Destination"))) { continue; } diff --git a/forge-gui/res/cardsfolder/c/creeping_chill.txt b/forge-gui/res/cardsfolder/c/creeping_chill.txt index b20e319bce6..6afe9895037 100644 --- a/forge-gui/res/cardsfolder/c/creeping_chill.txt +++ b/forge-gui/res/cardsfolder/c/creeping_chill.txt @@ -2,7 +2,7 @@ Name:Creeping Chill ManaCost:3 B Types:Sorcery A:SP$ DamageAll | StackDescription$ CARDNAME deals 3 damage to each opponent and | Cost$ 3 B | ValidPlayers$ Player.Opponent | NumDmg$ 3 | SubAbility$ DBGainLife | SpellDescription$ CARDNAME deals 3 damage to each opponent and you gain 3 life. -T:Mode$ ChangesZone | Origin$ Library | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigExile | OptionalDecider$ You | TriggerDescription$ When CARDNAME is put into your graveyard from your library, you may exile it. If you do, then a opponent and you gain 3 life. +T:Mode$ ChangesZone | Origin$ Library | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigExile | OptionalDecider$ You | TriggerDescription$ When CARDNAME is put into your graveyard from your library, you may exile it. If you do, CARDNAME deals 3 damage to each opponent and you gain 3 life. SVar:TrigExile:DB$ ChangeZone | Defined$ TriggeredCardLKICopy | Origin$ Graveyard | Destination$ Exile | SubAbility$ DBDamage SVar:DBDamage:DB$ DealDamage | Defined$ Player.Opponent | NumDmg$ 3 | SubAbility$ DBGainLife SVar:DBGainLife:DB$ GainLife | Defined$ You | LifeAmount$ 3 diff --git a/forge-gui/res/cardsfolder/k/kinzu_of_the_bleak_coven.txt b/forge-gui/res/cardsfolder/k/kinzu_of_the_bleak_coven.txt index 1a955d563a9..d512fda3610 100644 --- a/forge-gui/res/cardsfolder/k/kinzu_of_the_bleak_coven.txt +++ b/forge-gui/res/cardsfolder/k/kinzu_of_the_bleak_coven.txt @@ -4,6 +4,6 @@ Types:Legendary Creature Phyrexian Vampire PT:5/4 K:Flying T:Mode$ ChangesZone | ValidCard$ Creature.nonToken+Other+YouCtrl | Origin$ Battlefield | Destination$ Graveyard | TriggerZones$ Battlefield | Execute$ TrigExile | OptionalDecider$ You | TriggerDescription$ Whenever another nontoken creature you control dies, you may pay 2 life and exile it. If you do, create a token that's a copy of that creature, except it's 1/1 and has toxic 1. (Players dealt combat damage by it also get a poison counter.) -SVar:TrigExile:AB$ CopyPermanent | Cost$ PayLife<2> ExileFromGrave<1/Card.TriggeredCard/Exile nontoken creature that just died> | AddKeywords$ Toxic:1 | Defined$ TriggeredCardLKICopy | SetPower$ 1 | SetToughness$ 1 +SVar:TrigExile:AB$ CopyPermanent | Cost$ PayLife<2> ExileAnyGrave<1/Card.TriggeredCard/Exile nontoken creature that just died> | AddKeywords$ Toxic:1 | Defined$ TriggeredCardLKICopy | SetPower$ 1 | SetToughness$ 1 DeckHas:Ability$Token Oracle:Flying\nWhenever another nontoken creature you control dies, you may pay 2 life and exile it. If you do, create a token that's a copy of that creature, except it's 1/1 and has toxic 1. (Players dealt combat damage by it also get a poison counter.) diff --git a/forge-gui/res/cardsfolder/t/tawnoss_coffin.txt b/forge-gui/res/cardsfolder/t/tawnoss_coffin.txt index 480bc209678..e24734a4279 100644 --- a/forge-gui/res/cardsfolder/t/tawnoss_coffin.txt +++ b/forge-gui/res/cardsfolder/t/tawnoss_coffin.txt @@ -2,16 +2,12 @@ Name:Tawnos's Coffin ManaCost:4 Types:Artifact K:You may choose not to untap CARDNAME during your untap step. -A:AB$ Pump | Cost$ 3 T | ValidTgts$ Creature | ImprintCards$ Targeted | SubAbility$ DBRememberAura | StackDescription$ SpellDescription | SpellDescription$ Exile target creature and all Auras attached to it. Note the number and kind of counters that were on that creature. When CARDNAME leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent. -SVar:DBRememberAura:DB$ PumpAll | ValidCards$ Aura.AttachedTo Creature.IsImprinted | RememberAllPumped$ True | StackDescription$ None | SubAbility$ DBEffect -SVar:DBEffect:DB$ Effect | Triggers$ LeavesPlay,Untap | ImprintCards$ ParentTarget | RememberObjects$ Remembered | NoteCounterDefined$ Imprinted | Duration$ Permanent | SubAbility$ DBExile -SVar:DBExile:DB$ ChangeZoneAll | Origin$ Battlefield | Destination$ Exile | ChangeType$ Card.IsRemembered,Card.IsImprinted | SubAbility$ DBCleanup -SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True | ClearImprinted$ True -SVar:LeavesPlay:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.EffectSource | Execute$ RestoreCounters | TriggerController$ TriggeredCardController | TriggerDescription$ When EFFECTSOURCE leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent. -SVar:Untap:Mode$ Untaps | ValidCard$ Card.EffectSource | Execute$ TrigReturn | TriggerController$ TriggeredCardController | Secondary$ True | TriggerDescription$ When EFFECTSOURCE leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent. -SVar:TrigReturn:DB$ ChangeZone | Defined$ Imprinted | Origin$ Exile | Destination$ Battlefield | Tapped$ True | SubAbility$ RestoreCounters -SVar:RestoreCounters:DB$ NoteCounters | Mode$ Load | Defined$ Imprinted | SubAbility$ TrigAuraReturn -SVar:TrigAuraReturn:DB$ ChangeZone | Defined$ Remembered | Origin$ Exile | Destination$ Battlefield | AttachedTo$ Valid Creature.IsImprinted | SubAbility$ ExileSelf -SVar:ExileSelf:DB$ ChangeZone | Origin$ Command | Destination$ Exile | Defined$ Self +A:AB$ Effect | Cost$ 3 T | ValidTgts$ Creature | Triggers$ LeavesPlay,Untap | ImprintCards$ Targeted | RememberObjects$ Valid Aura.AttachedTo Targeted | NoteCounterDefined$ Targeted | Duration$ Permanent | SubAbility$ DBExile | StackDescription$ SpellDescription | SpellDescription$ Exile target creature and all Auras attached to it. Note the number and kind of counters that were on that creature. When CARDNAME leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent. +SVar:DBExile:DB$ ChangeZoneAll | Origin$ Battlefield | Destination$ Exile | ChangeType$ Targeted.Self,Aura.AttachedTo Card.Self +SVar:LeavesPlay:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.EffectSource | Execute$ TrigCounters | TriggerController$ TriggeredCardController | OneOff$ True | TriggerDescription$ When EFFECTSOURCE leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent. +SVar:Untap:Mode$ Untaps | ValidCard$ Card.EffectSource | Execute$ TrigCounters | TriggerController$ TriggeredCardController | OneOff$ True | Secondary$ True | TriggerDescription$ When EFFECTSOURCE leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent. +SVar:TrigCounters:DB$ NoteCounters | Mode$ Load | Defined$ Imprinted | SubAbility$ TrigReturn +SVar:TrigReturn:DB$ ChangeZone | Defined$ Imprinted | Origin$ Exile | Destination$ Battlefield | Tapped$ True | SubAbility$ TrigAuraReturn +SVar:TrigAuraReturn:DB$ ChangeZone | Defined$ Remembered | Origin$ Exile | Destination$ Battlefield | AttachedTo$ Imprinted AI:RemoveDeck:All Oracle:You may choose not to untap Tawnos's Coffin during your untap step.\n{3}, {T}: Exile target creature and all Auras attached to it. Note the number and kind of counters that were on that creature. When Tawnos's Coffin leaves the battlefield or becomes untapped, return that exiled card to the battlefield under its owner's control tapped with the noted number and kind of counters on it. If you do, return the other exiled cards to the battlefield under their owner's control attached to that permanent. diff --git a/forge-gui/res/cardsfolder/t/timothar_baron_of_bats.txt b/forge-gui/res/cardsfolder/t/timothar_baron_of_bats.txt index 1de0c71fd52..c77b7f40f12 100644 --- a/forge-gui/res/cardsfolder/t/timothar_baron_of_bats.txt +++ b/forge-gui/res/cardsfolder/t/timothar_baron_of_bats.txt @@ -4,7 +4,7 @@ Types:Legendary Creature Vampire Noble PT:4/4 K:Ward:Discard<1/Card> T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Vampire.Other+nonToken+YouCtrl | TriggerZones$ Battlefield | Execute$ TrigToken | TriggerDescription$ Whenever another nontoken Vampire you control dies, you may pay {1} and exile it. If you do, create a 1/1 black Bat creature token with flying. It gains "When this creature deals combat damage to a player, sacrifice it and return the exiled card to the battlefield tapped." -SVar:TrigToken:AB$ Token | Cost$ 1 ExileFromGrave<1/Card.TriggeredNewCard/the Vampire card> | TokenRemembered$ ExiledCards | TokenScript$ b_1_1_bat_flying | ImprintTokens$ True | SubAbility$ DBAnimate +SVar:TrigToken:AB$ Token | Cost$ 1 ExileAnyGrave<1/Card.TriggeredNewCard/the Vampire card> | TokenRemembered$ ExiledCards | TokenScript$ b_1_1_bat_flying | ImprintTokens$ True | SubAbility$ DBAnimate SVar:DBAnimate:DB$ Animate | Defined$ Imprinted | Duration$ Permanent | Triggers$ CDTrigger SVar:CDTrigger:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigSac | TriggerZones$ Battlefield | TriggerDescription$ When this creature deals combat damage to a player, sacrifice it and return the exiled card to the battlefield tapped. SVar:TrigSac:DB$ Sacrifice | SubAbility$ DBReturn diff --git a/forge-gui/src/main/java/forge/player/HumanPlay.java b/forge-gui/src/main/java/forge/player/HumanPlay.java index 6e4fb54ba99..31566a9ebdd 100644 --- a/forge-gui/src/main/java/forge/player/HumanPlay.java +++ b/forge-gui/src/main/java/forge/player/HumanPlay.java @@ -320,7 +320,13 @@ public class HumanPlay { costExile.payAsDecided(p, PaymentDecision.card(p.getCardsIn(ZoneType.Graveyard)), sourceAbility, hcd.isEffect()); } else { from = costExile.getFrom(); - CardCollection list = CardLists.getValidCards(p.getCardsIn(from), part.getType().split(";"), p, source, sourceAbility); + CardCollection list; + if (costExile.zoneRestriction != 1) { + list = new CardCollection(p.getGame().getCardsIn(from)); + } else { + list = new CardCollection(p.getCardsIn(from)); + } + list = CardLists.getValidCards(list, part.getType().split(";"), p, source, sourceAbility); final int nNeeded = getAmountFromPart(part, source, sourceAbility); if (list.size() < nNeeded) { return false;