diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java index a6202b9f20a..e41ff259f19 100644 --- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java @@ -264,6 +264,20 @@ public class AiCostDecision extends CostDecisionMakerBase { return PaymentDecision.number(c); } + @Override + public PaymentDecision visit(final CostForage cost) { + CardCollection food = CardLists.filter(player.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Food"), CardPredicates.canBeSacrificedBy(ability, isEffect())); + CardCollection exile = CardLists.filter(player.getCardsIn(ZoneType.Graveyard), CardPredicates.canExiledBy(ability, isEffect())); + if (!food.isEmpty()) { + final AiController aic = ((PlayerControllerAi)player.getController()).getAi(); + CardCollectionView list = aic.chooseSacrificeType("Food", ability, isEffect(), 1, null); + return list == null ? null : PaymentDecision.card(list); + } else { + CardCollectionView chosen = ComputerUtil.chooseExileFromList(player, exile, source, 3, ability, isEffect()); + return null == chosen ? null : PaymentDecision.card(chosen); + } + } + @Override public PaymentDecision visit(CostRollDice cost) { int c = cost.getAbilityAmount(ability); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index a8c1b86d9f3..1295698581c 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -708,6 +708,11 @@ public class ComputerUtil { typeList = new CardCollection(ai.getCardsIn(cost.from)); } typeList = CardLists.getValidCards(typeList, cost.getType().split(";"), activate.getController(), activate, sa); + + return chooseExileFromList(ai, typeList, activate, amount, sa, effect); + } + + public static CardCollection chooseExileFromList(final Player ai, CardCollection typeList, final Card activate, final int amount, SpellAbility sa, final boolean effect) { typeList = CardLists.filter(typeList, CardPredicates.canExiledBy(sa, effect)); // don't exile the card we're pumping diff --git a/forge-game/src/main/java/forge/game/cost/Cost.java b/forge-game/src/main/java/forge/game/cost/Cost.java index d93c9c57896..503e450b93d 100644 --- a/forge-game/src/main/java/forge/game/cost/Cost.java +++ b/forge-game/src/main/java/forge/game/cost/Cost.java @@ -553,6 +553,10 @@ public class Cost implements Serializable { return new CostRevealChosen(splitStr[0], splitStr.length > 1 ? splitStr[1] : null); } + if (parse.equals("Forage")) { + return new CostForage(); + } + // These won't show up with multiples if (parse.equals("Untap") || parse.equals("Q")) { return new CostUntap(); diff --git a/forge-game/src/main/java/forge/game/cost/CostForage.java b/forge-game/src/main/java/forge/game/cost/CostForage.java new file mode 100644 index 00000000000..ceb1fc9932f --- /dev/null +++ b/forge-game/src/main/java/forge/game/cost/CostForage.java @@ -0,0 +1,91 @@ +package forge.game.cost; + +import java.util.Map; + +import forge.game.Game; +import forge.game.ability.AbilityKey; +import forge.game.ability.SpellAbilityEffect; +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.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.trigger.TriggerType; +import forge.game.zone.ZoneType; + +public class CostForage extends CostPartWithList { + + private static final long serialVersionUID = 1L; + + @Override + public boolean canPay(SpellAbility ability, Player payer, boolean effect) { + CardCollection graveyard = CardLists.filter(payer.getCardsIn(ZoneType.Graveyard), CardPredicates.canExiledBy(ability, effect)); + if (graveyard.size() >= 3) { + return true; + } + + CardCollection food = CardLists.filter(payer.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Food"), CardPredicates.canBeSacrificedBy(ability, effect)); + if (!food.isEmpty()) { + return true; + } + + return false; + } + + @Override + public String toString() { + return "Forage"; + } + + @Override + protected Card doPayment(Player payer, SpellAbility ability, Card targetCard, final boolean effect) { return null; } + @Override + protected boolean canPayListAtOnce() { return true; } + @Override + protected CardCollectionView doListPayment(Player payer, SpellAbility ability, CardCollectionView targetCards, final boolean effect) { + final Game game = payer.getGame(); + if (targetCards.size() == 3) { + Map moveParams = AbilityKey.newMap(); + AbilityKey.addCardZoneTableParams(moveParams, table); + CardCollection result = new CardCollection(); + for (Card targetCard : targetCards) { + Card newCard = game.getAction().exile(targetCard, null, moveParams); + result.add(newCard); + SpellAbilityEffect.handleExiledWith(newCard, ability); + } + triggerForage(payer); + return result; + } else if (targetCards.size() == 1) { + Map moveParams = AbilityKey.newMap(); + AbilityKey.addCardZoneTableParams(moveParams, table); + CardCollection result = new CardCollection(game.getAction().sacrifice(targetCards.getFirst(), ability, effect, moveParams)); + triggerForage(payer); + return result; + } else { + return null; + } + } + + protected void triggerForage(Player payer) { + final Map runParams = AbilityKey.mapFromPlayer(payer); + payer.getGame().getTriggerHandler().runTrigger(TriggerType.Forage, runParams, false); + } + + public static final String HashLKIListKey = "Foraged"; + public static final String HashCardListKey = "ForagedCards"; + + @Override + public String getHashForLKIList() { + return HashLKIListKey; + } + @Override + public String getHashForCardList() { + return HashCardListKey; + } + + public T accept(ICostVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/forge-game/src/main/java/forge/game/cost/ICostVisitor.java b/forge-game/src/main/java/forge/game/cost/ICostVisitor.java index 5b6b319ab3e..2286ec1de70 100644 --- a/forge-game/src/main/java/forge/game/cost/ICostVisitor.java +++ b/forge-game/src/main/java/forge/game/cost/ICostVisitor.java @@ -15,6 +15,7 @@ public interface ICostVisitor { T visit(CostExert cost); T visit(CostEnlist cost); T visit(CostFlipCoin cost); + T visit(CostForage cost); T visit(CostRollDice cost); T visit(CostMill cost); T visit(CostAddMana cost); @@ -103,6 +104,10 @@ public interface ICostVisitor { public T visit(CostFlipCoin cost) { return null; } + @Override + public T visit(CostForage cost) { + return null; + } @Override public T visit(CostRollDice cost) { @@ -209,5 +214,4 @@ public interface ICostVisitor { return null; } } - } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerForage.java b/forge-game/src/main/java/forge/game/trigger/TriggerForage.java new file mode 100644 index 00000000000..8a41fc60092 --- /dev/null +++ b/forge-game/src/main/java/forge/game/trigger/TriggerForage.java @@ -0,0 +1,37 @@ +package forge.game.trigger; + +import java.util.Map; + +import forge.game.ability.AbilityKey; +import forge.game.card.Card; +import forge.game.spellability.SpellAbility; +import forge.util.Localizer; + +public class TriggerForage extends Trigger { + + public TriggerForage(Map params, Card host, boolean intrinsic) { + super(params, host, intrinsic); + } + + @Override + public boolean performTest(Map runParams) { + if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) { + return false; + } + + return true; + } + + @Override + public void setTriggeringObjects(SpellAbility sa, Map runParams) { + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player); + } + + @Override + public String getImportantStackObjects(SpellAbility sa) { + StringBuilder sb = new StringBuilder(); + sb.append(Localizer.getInstance().getMessage("lblPlayer")).append(": ").append(sa.getTriggeringObject(AbilityKey.Player)); + return sb.toString(); + } + +} diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerType.java b/forge-game/src/main/java/forge/game/trigger/TriggerType.java index d14dfa4ee21..040b636b0b5 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerType.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerType.java @@ -83,6 +83,7 @@ public enum TriggerType { Fight(TriggerFight.class), FightOnce(TriggerFightOnce.class), FlippedCoin(TriggerFlippedCoin.class), + Forage(TriggerForage.class), Foretell(TriggerForetell.class), Immediate(TriggerImmediate.class), Investigated(TriggerInvestigated.class), diff --git a/forge-gui/res/cardsfolder/upcoming/treetop_sentries.txt b/forge-gui/res/cardsfolder/upcoming/treetop_sentries.txt new file mode 100644 index 00000000000..47c1b0e39d1 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/treetop_sentries.txt @@ -0,0 +1,9 @@ +Name:Treetop Sentries +ManaCost:3 G +Types:Creature Squirrel Archer +PT:2/4 +K:Reach +T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigDraw | TriggerDescription$ When CARDNAME enters, you may forage. If you do, draw a card. (To forage, exile three cards from your graveyard or sacrifice a Food.) +SVar:TrigDraw:AB$ Draw | Cost$ Forage | NumCards$ 1 +Oracle:Reach\nWhen Treetop Sentries enters, you may forage. If you do, draw a card. (To forage, exile three cards from your graveyard or sacrifice a Food.) + diff --git a/forge-gui/src/main/java/forge/player/HumanCostDecision.java b/forge-gui/src/main/java/forge/player/HumanCostDecision.java index 3307cb0f915..bd253a601ce 100644 --- a/forge-gui/src/main/java/forge/player/HumanCostDecision.java +++ b/forge-gui/src/main/java/forge/player/HumanCostDecision.java @@ -572,6 +572,36 @@ public class HumanCostDecision extends CostDecisionMakerBase { return PaymentDecision.number(c); } + @Override + public PaymentDecision visit(final CostForage cost) { + CardCollection food = CardLists.filter(player.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Food"), CardPredicates.canBeSacrificedBy(ability, isEffect())); + CardCollection exile = CardLists.filter(player.getCardsIn(ZoneType.Graveyard), CardPredicates.canExiledBy(ability, isEffect())); + if (!food.isEmpty() && confirmAction(cost, "Sacrifice Food")) { + // Sacrifice Food logic + final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, 1, 1, food, ability); + inp.setMessage(Localizer.getInstance().getMessage("lblSelectATargetToSacrifice", "Food", "%d")); + inp.setCancelAllowed(!mandatory); + inp.showAndWait(); + if (inp.hasCancelled()) { + return null; + } + + return PaymentDecision.card(inp.getSelected()); + } if (exile.size() >= 3) { + // Sacrifice Food logic + final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, 3, 3, exile, ability); + inp.setMessage(Localizer.getInstance().getMessage("lblSelectToExile", 3)); + inp.setCancelAllowed(!mandatory); + inp.showAndWait(); + if (inp.hasCancelled()) { + return null; + } + + return PaymentDecision.card(inp.getSelected()); + } + return null; + } + @Override public PaymentDecision visit(final CostRollDice cost) { int c = cost.getAbilityAmount(ability);