Add Forage Cost (#5633)

* Add Forage Cost

* ~ fix PaymentDecision for now

* TriggerForage: add Forage trigger

* CostForage: Add First Part of Human Sacrifice Food Logic

* Human ready, first Forage card

* AiCostDecision: Basic AI for Forage
This commit is contained in:
Hans Mackowiak
2024-07-20 16:25:03 +02:00
committed by GitHub
parent f758a4127d
commit 95ae25e972
9 changed files with 196 additions and 1 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -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();

View File

@@ -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<AbilityKey, Object> 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<AbilityKey, Object> 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<AbilityKey, Object> 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> T accept(ICostVisitor<T> visitor) {
return visitor.visit(this);
}
}

View File

@@ -15,6 +15,7 @@ public interface ICostVisitor<T> {
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<T> {
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<T> {
return null;
}
}
}

View File

@@ -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<String, String> params, Card host, boolean intrinsic) {
super(params, host, intrinsic);
}
@Override
public boolean performTest(Map<AbilityKey, Object> runParams) {
if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) {
return false;
}
return true;
}
@Override
public void setTriggeringObjects(SpellAbility sa, Map<AbilityKey, Object> 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();
}
}

View File

@@ -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),

View File

@@ -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.)

View File

@@ -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);