Skip to content

Commit

Permalink
Alternative Cost For ActivatedAbilities and Spells (#5037)
Browse files Browse the repository at this point in the history
* Rework Alternative Cost + MayPlay
  • Loading branch information
Hanmac authored Apr 15, 2024
1 parent d3e471f commit 4d26228
Show file tree
Hide file tree
Showing 49 changed files with 262 additions and 184 deletions.
2 changes: 1 addition & 1 deletion forge-ai/src/main/java/forge/ai/AiAttackController.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public boolean apply(Card c) {
continue;
}
sa.setActivatingPlayer(defender);
if (sa.hasParam("Crew") && !ComputerUtilCost.checkTapTypeCost(defender, sa.getPayCosts(), c, sa, tappedDefenders)) {
if (sa.isCrew() && !ComputerUtilCost.checkTapTypeCost(defender, sa.getPayCosts(), c, sa, tappedDefenders)) {
continue;
} else if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
continue;
Expand Down
8 changes: 2 additions & 6 deletions forge-ai/src/main/java/forge/ai/ComputerUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -791,11 +791,7 @@ public static CardCollection chooseTapTypeAccumulatePower(final Player ai, final
all.removeAll(exclude);
CardCollection typeList = CardLists.getValidCards(all, type.split(";"), activate.getController(), activate, sa);

if (sa.hasParam("Crew")) {
typeList = CardLists.getNotKeyword(typeList, "CARDNAME can't crew Vehicles.");
}

typeList = CardLists.filter(typeList, Presets.CAN_TAP);
typeList = CardLists.filter(typeList, sa.isCrew() ? Presets.CAN_CREW : Presets.CAN_TAP);

if (tap) {
typeList.remove(activate);
Expand All @@ -817,7 +813,7 @@ public static CardCollection chooseTapTypeAccumulatePower(final Player ai, final
tapList.clear();
}
tapList.add(next);
totalPower = CardLists.getTotalPower(tapList, true, sa.hasParam("Crew"));
totalPower = CardLists.getTotalPower(tapList, true, sa.isCrew());
if (totalPower >= amount) {
break;
}
Expand Down
2 changes: 1 addition & 1 deletion forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ public static boolean checkTapTypeCost(final Player ai, final Cost cost, final C
* - block against evasive (flyers, intimidate, etc.)
* - break board stall by racing with evasive vehicle
*/
if (sa.hasParam("Crew")) {
if (sa.isCrew()) {
Card vehicle = AnimateAi.becomeAnimated(source, sa);
final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle);
String totalP = type.split("withTotalPowerGE")[1];
Expand Down
2 changes: 1 addition & 1 deletion forge-ai/src/main/java/forge/ai/ability/AnimateAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
}

if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
if (sa.hasParam("Crew") && c.isCreature()) {
if (sa.isCrew() && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature
return false;
}
Expand Down
2 changes: 2 additions & 0 deletions forge-game/src/main/java/forge/game/ForgeScript.java
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ public static boolean spellAbilityHasProperty(SpellAbility sa, String property,
return sa.isBuyback();
} else if (property.equals("Craft")) {
return sa.isCraft();
} else if (property.equals("Crew")) {
return sa.isCrew();
} else if (property.equals("Cycling")) {
return sa.isCycling();
} else if (property.equals("Dash")) {
Expand Down
206 changes: 87 additions & 119 deletions forge-game/src/main/java/forge/game/GameActionUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,18 @@
import com.google.common.collect.*;
import forge.game.card.*;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAlternativeCost;
import forge.util.Aggregates;
import org.apache.commons.lang3.StringUtils;

import forge.card.MagicColor;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser;
import forge.game.ability.AbilityFactory;
import forge.game.ability.ApiType;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.CardPlayOption.PayManaCost;
import forge.game.cost.Cost;
import forge.game.cost.CostPayment;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
Expand Down Expand Up @@ -109,71 +108,19 @@ public static final List<SpellAbility> getAlternativeCosts(final SpellAbility sa
game.getAction().checkStaticAbilities(false, Sets.newHashSet(source), preList);
}

for (CardPlayOption o : source.mayPlay(activator)) {
// do not appear if it can be cast with SorcerySpeed
if (o.getAbility().hasParam("MayPlayNotSorcerySpeed") && activator.canCastSorcery()) {
continue;
}
// non basic are only allowed if PayManaCost is yes
if ((!sa.isBasicSpell() || (sa.costHasManaX() && sa.getPayCosts().getCostMana() != null
&& sa.getPayCosts().getCostMana().getXMin() > 0)) && o.getPayManaCost() == PayManaCost.NO) {
continue;
}
final Card host = o.getHost();

SpellAbility newSA = null;

boolean changedManaCost = false;
if (o.getPayManaCost() == PayManaCost.NO) {
newSA = sa.copyWithNoManaCost(activator);
newSA.setBasicSpell(false);
changedManaCost = true;
} else if (o.getAltManaCost() != null) {
newSA = sa.copyWithManaCostReplaced(activator, o.getAltManaCost());
newSA.setBasicSpell(false);
changedManaCost = true;
} else {
if (altCostOnly) {
continue;
}
newSA = sa.copy(activator);
}

if (o.getAbility().hasParam("ValidAfterStack")) {
newSA.getMapParams().put("ValidAfterStack", o.getAbility().getParam("ValidAfterStack"));
}

final SpellAbilityRestriction sar = newSA.getRestrictions();
if (o.isWithFlash()) {
sar.setInstantSpeed(true);
}
sar.setZone(null);
newSA.setMayPlay(o);

if (changedManaCost) {
if ("0".equals(sa.getParam("ActivationLimit")) && sa.getHostCard().getManaCost().isNoCost()) {
sar.setLimitToCheck(null);
}
}

final StringBuilder sb = new StringBuilder(sa.getDescription());
if (!source.equals(host)) {
sb.append(" by ");
if (host.isImmutable() && host.getEffectSource() != null) {
sb.append(host.getEffectSource());
} else {
sb.append(host);
}
}
if (o.getAbility().hasParam("MayPlayText")) {
sb.append(" (").append(o.getAbility().getParam("MayPlayText")).append(")");
// Alt Cost only for Basic Spells
if (sa.isBasicSpell()) {
for (SpellAbility newSA : StaticAbilityAlternativeCost.alternativeCosts(sa, source, activator)) {
alternatives.add(newSA);
// should only add MayPlay Zones
alternatives.addAll(getMayPlaySpellOptions(newSA, source, activator, altCostOnly));
}
sb.append(o.toString(false));
newSA.setDescription(sb.toString());
alternatives.add(newSA);
}

alternatives.addAll(getMayPlaySpellOptions(sa, source, activator, altCostOnly));

// need to be done there before static abilities does reset the card
// These Keywords depend on the Mana Cost of for Split Cards
if (sa.isBasicSpell()) {
for (final KeywordInterface inst : source.getKeywords()) {
final String keyword = inst.getOriginal();
Expand Down Expand Up @@ -279,32 +226,30 @@ public static final List<SpellAbility> getAlternativeCosts(final SpellAbility sa
}

// some needs to check after ability was put on the stack
// Currently this is only checked for Toolbox and that only cares about creature spells
if (source.isCreature() && game.getAction().hasStaticAbilityAffectingZone(ZoneType.Stack, StaticAbilityLayer.ABILITIES)) {
if (game.getAction().hasStaticAbilityAffectingZone(ZoneType.Stack, StaticAbilityLayer.ABILITIES)) {
Zone oldZone = source.getLastKnownZone();
Card blitzCopy = source;
Card stackCopy = source;
if (!source.isLKI()) {
blitzCopy = CardCopyService.getLKICopy(source);
stackCopy = CardCopyService.getLKICopy(source);
}
blitzCopy.setLastKnownZone(game.getStackZone());
stackCopy.setLastKnownZone(game.getStackZone());
lkicheck = true;

blitzCopy.clearStaticChangedCardKeywords(false);
CardCollection preList = new CardCollection(blitzCopy);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(blitzCopy), preList);
stackCopy.clearStaticChangedCardKeywords(false);
CardCollection preList = new CardCollection(stackCopy);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(stackCopy), preList);

// currently only for Keyword BLitz, but should affect Dash probably too
for (final KeywordInterface inst : blitzCopy.getKeywords(Keyword.BLITZ)) {
// TODO with mana value 4 or greater has blitz.
for (final KeywordInterface inst : stackCopy.getUnhiddenKeywords()) {
for (SpellAbility iSa : inst.getAbilities()) {
// do only non intrinsic
if (!iSa.isIntrinsic()) {
if (iSa.isSpell() && !iSa.isIntrinsic()) {
alternatives.add(iSa);
alternatives.addAll(StaticAbilityAlternativeCost.alternativeCosts(iSa, source, activator));
}
}
}
// need to reset to Old Zone, or canPlay would fail
blitzCopy.setLastKnownZone(oldZone);
stackCopy.setLastKnownZone(oldZone);
}
}

Expand All @@ -328,61 +273,84 @@ public static final List<SpellAbility> getAlternativeCosts(final SpellAbility sa
}
alternatives.add(newSA);
}
// alternative Cost for activated abilities
alternatives.addAll(StaticAbilityAlternativeCost.alternativeCosts(sa, source, activator));
}

// below are for some special cases of activated abilities
if (sa.isCycling() && activator.hasKeyword("CyclingForZero")) {
for (final KeywordInterface inst : source.getKeywords()) {
// need to find the correct Keyword from which this Ability is from
if (!inst.getAbilities().contains(sa)) {
continue;
}

// set the cost to this directly to bypass non mana cost
final SpellAbility newSA = sa.copyWithDefinedCost("Discard<1/CARDNAME>");
newSA.setActivatingPlayer(activator);
newSA.putParam("CostDesc", ManaCostParser.parse("0"));
return alternatives;
}

// need to build a new Keyword to get better Reminder Text
String data[] = inst.getOriginal().split(":");
data[1] = "0";
KeywordInterface newKi = Keyword.getInstance(StringUtils.join(data, ":"));
public static List<SpellAbility> getMayPlaySpellOptions(final SpellAbility sa, final Card source, final Player activator, boolean altCostOnly) {
final List<SpellAbility> alternatives = Lists.newArrayList();

// makes new SpellDescription
final StringBuilder sb = new StringBuilder();
sb.append(newSA.getCostDescription());
sb.append("(").append(newKi.getReminderText()).append(")");
newSA.setDescription(sb.toString());
if (sa.isSpell() && source.isInPlay()) {
return alternatives;
}

alternatives.add(newSA);
for (CardPlayOption o : source.mayPlay(activator)) {
// do not appear if it can be cast with SorcerySpeed
if (o.getAbility().hasParam("MayPlayNotSorcerySpeed") && activator.canCastSorcery()) {
continue;
}
// non basic are only allowed if PayManaCost is yes
if ((!sa.isBasicSpell() || (sa.costHasManaX() && sa.getPayCosts().getCostMana() != null
&& sa.getPayCosts().getCostMana().getXMin() > 0)) && o.getPayManaCost() == PayManaCost.NO) {
continue;
}
final Card host = o.getHost();

SpellAbility newSA = null;

boolean changedManaCost = false;
if (o.getPayManaCost() == PayManaCost.NO) {
newSA = sa.copyWithNoManaCost(activator);
newSA.setBasicSpell(false);
changedManaCost = true;
} else if (o.getAltManaCost() != null) {
newSA = sa.copyWithManaCostReplaced(activator, o.getAltManaCost());
newSA.setBasicSpell(false);
changedManaCost = true;
} else {
if (altCostOnly) {
continue;
}
newSA = sa.copy(activator);
}
if (sa.isEquip() && activator.hasKeyword("You may pay 0 rather than pay equip costs.")) {
for (final KeywordInterface inst : source.getKeywords()) {
// need to find the correct Keyword from which this Ability is from
if (!inst.getAbilities().contains(sa)) {
continue;
}

// set the cost to this directly to bypass non mana cost
SpellAbility newSA = sa.copyWithDefinedCost("0");
newSA.setActivatingPlayer(activator);
newSA.putParam("CostDesc", ManaCostParser.parse("0"));
if (o.getAbility().hasParam("ValidAfterStack")) {
newSA.getMapParams().put("ValidAfterStack", o.getAbility().getParam("ValidAfterStack"));
}

// need to build a new Keyword to get better Reminder Text
String data[] = inst.getOriginal().split(":");
data[1] = "0";
KeywordInterface newKi = Keyword.getInstance(StringUtils.join(data, ":"));
final SpellAbilityRestriction sar = newSA.getRestrictions();
if (o.isWithFlash()) {
sar.setInstantSpeed(true);
}
sar.setZone(null);
newSA.setMayPlay(o);

// makes new SpellDescription
final StringBuilder sb = new StringBuilder();
sb.append(newSA.getCostDescription());
sb.append("(").append(newKi.getReminderText()).append(")");
newSA.setDescription(sb.toString());
if (changedManaCost) {
if ("0".equals(sa.getParam("ActivationLimit")) && sa.getHostCard().getManaCost().isNoCost()) {
sar.setLimitToCheck(null);
}
}

alternatives.add(newSA);
final StringBuilder sb = new StringBuilder(sa.getDescription());
if (!source.equals(host)) {
sb.append(" by ");
if (host.isImmutable() && host.getEffectSource() != null) {
sb.append(host.getEffectSource());
} else {
sb.append(host);
}
}
if (o.getAbility().hasParam("MayPlayText")) {
sb.append(" (").append(o.getAbility().getParam("MayPlayText")).append(")");
}
sb.append(o.toString(false));
newSA.setDescription(sb.toString());
alternatives.add(newSA);
}

return alternatives;
}

Expand Down Expand Up @@ -886,7 +854,7 @@ public static void rollbackAbility(SpellAbility ability, final Zone fromZone, fi
if (fromZone != null) { // and not a copy
// might have been an alternative lki host
oldCard = ability.getCardState().getCard();

oldCard.setCastSA(null);
oldCard.setCastFrom(null);
// add back to where it came from, hopefully old state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ public void resolve(final SpellAbility sa) {
c.addImprintedCards(AbilityUtils.getDefinedCards(source, animateImprinted, sa));
}

if (sa.hasParam("Crew")) {
if (sa.isCrew()) {
c.becomesCrewed(sa);
c.updatePowerToughnessForView();
}
Expand Down
4 changes: 4 additions & 0 deletions forge-game/src/main/java/forge/game/card/Card.java
Original file line number Diff line number Diff line change
Expand Up @@ -6529,6 +6529,10 @@ public final boolean canSpecialize() {
return getRules() != null && getRules().getSplitType() == CardSplitType.Specialize;
}

public boolean canCrew() {
return canTap() && !StaticAbilityCantCrew.cantCrew(this);
}

public int getTimesCrewedThisTurn() {
return timesCrewedThisTurn;
}
Expand Down
4 changes: 2 additions & 2 deletions forge-game/src/main/java/forge/game/card/CardFactoryUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -3654,8 +3654,8 @@ public void resolve() {

// tapXType has a special check for withTotalPower, and NEEDS it to be "+withTotalPowerGE"
String effect = "AB$ Animate | Cost$ tapXType<Any/Creature.Other+withTotalPowerGE" + power + "> | " +
"CostDesc$ Crew " + power + " (Tap any number of creatures you control with total power " + power +
" or more: | Crew$ True | Secondary$ True | Defined$ Self | Types$ Artifact,Creature | " +
"PrecostDesc$ Crew | CostDesc$ " + power + " (Tap any number of creatures you control with total power " + power +
" or more: | Secondary$ True | Defined$ Self | Types$ Artifact,Creature | " +
"SpellDescription$ CARDNAME becomes an artifact creature until end of turn.)";
if (k.length > 2) {
effect += " | " + k[2];
Expand Down
7 changes: 7 additions & 0 deletions forge-game/src/main/java/forge/game/card/CardPredicates.java
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,13 @@ public boolean apply(Card c) {
return c.canTap();
}
};

public static final Predicate<Card> CAN_CREW = new Predicate<Card>() {
@Override
public boolean apply(Card c) {
return c.canCrew();
}
};
/**
* a Predicate<Card> to get all creatures.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public Integer getMaxAmountX(final SpellAbility ability, final Player payer, fin
@Override
public final String toString() {
final StringBuilder sb = new StringBuilder();
if (this.counter.is(CounterEnumType.LOYALTY)) {
if (this.counter.is(CounterEnumType.LOYALTY) && payCostFromSource()) {
sb.append("-").append(this.getAmount());
} else {
sb.append("Remove ");
Expand Down
Loading

0 comments on commit 4d26228

Please sign in to comment.