forge.game.GameActionUtil.java Source code

Java tutorial

Introduction

Here is the source code for forge.game.GameActionUtil.java

Source

/*
 * Forge: Play Magic: the Gathering.
 * Copyright (C) 2011  Forge Team
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package forge.game;

import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

import forge.card.MagicColor;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityFactory.AbilityRecordType;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPlayOption;
import forge.game.card.CardPredicates;
import forge.game.card.CardPlayOption.PayManaCost;
import forge.game.cost.Cost;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.player.Player;
import forge.game.spellability.*;
import forge.game.zone.ZoneType;
import forge.util.TextUtil;

import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * GameActionUtil class.
 * </p>
 * 
 * @author Forge
 * @version $Id: GameActionUtil.java 29414 2015-05-17 11:28:21Z elcnesh $
 */
public final class GameActionUtil {
    // Cache these instead of generating them on the fly, to avoid excessive allocations every time
    // static abilities are checked.
    @SuppressWarnings("unchecked")
    private static final Map<String, String>[] BASIC_LAND_ABILITIES_PARAMS = new Map[MagicColor.WUBRG.length];
    private static final AbilityRecordType[] BASIC_LAND_ABILITIES_TYPES = new AbilityRecordType[MagicColor.WUBRG.length];
    static {
        for (int i = 0; i < MagicColor.WUBRG.length; i++) {
            String color = MagicColor.toShortString(MagicColor.WUBRG[i]);
            String abString = "AB$ Mana | Cost$ T | Produced$ " + color + " | SpellDescription$ Add {" + color
                    + "} to your mana pool.";
            Map<String, String> mapParams = AbilityFactory.getMapParams(abString);
            BASIC_LAND_ABILITIES_PARAMS[i] = mapParams;
            BASIC_LAND_ABILITIES_TYPES[i] = AbilityRecordType.getRecordType(mapParams);
        }
    }

    private GameActionUtil() {
        throw new AssertionError();
    }

    // restricted to combat damage, restricted to players
    /**
     * <p>
     * executeCombatDamageToPlayerEffects.
     * </p>
     * 
     * @param player
     *            a {@link forge.game.player.Player} object.
     * @param c
     *            a {@link forge.game.card.Card} object.
     * @param damage
     *            a int.
     */
    public static void executeCombatDamageToPlayerEffects(final Player player, final Card c, final int damage) {

        if (damage <= 0) {
            return;
        }

        for (final String key : c.getKeywords()) {
            if (!key.startsWith("Poisonous "))
                continue;
            final String[] k = key.split(" ", 2);
            final int poison = Integer.parseInt(k[1]);
            // Now can be copied by Strionic Resonator
            String effect = "AB$ Poison | Cost$ 0 | Defined$ PlayerNamed_" + player.getName() + " | Num$ " + k[1];
            SpellAbility ability = AbilityFactory.getAbility(effect, c);

            final StringBuilder sb = new StringBuilder();
            sb.append(c);
            sb.append(" - Poisonous: ");
            sb.append(player);
            sb.append(" gets ").append(poison).append(" poison counter");
            if (poison != 1) {
                sb.append("s");
            }
            sb.append(".");

            ability.setActivatingPlayer(c.getController());
            ability.setDescription(sb.toString());
            ability.setStackDescription(sb.toString());
            ability.setTrigger(true);

            player.getGame().getStack().addSimultaneousStackEntry(ability);

        }

        c.getDamageHistory().registerCombatDamage(player);
    } // executeCombatDamageToPlayerEffects

    /**
     * Gets the st land mana abilities.
     * @param game
     * 
     * @return the stLandManaAbilities
     */
    public static void grantBasicLandsManaAbilities(List<Card> lands) {
        // remove all abilities granted by this Command
        for (final Card land : lands) {
            List<SpellAbility> origManaAbs = Lists.newArrayList(land.getManaAbilities());
            // will get comodification exception without a different list
            for (final SpellAbility sa : origManaAbs) {
                if (sa.isBasicLandAbility()) {
                    land.getCurrentState().removeManaAbility(sa);
                }
            }
        }

        // add all appropriate mana abilities based on current types
        for (int i = 0; i < MagicColor.WUBRG.length; i++) {
            String landType = MagicColor.Constant.BASIC_LANDS.get(i);
            Map<String, String> mapParams = BASIC_LAND_ABILITIES_PARAMS[i];
            AbilityRecordType type = BASIC_LAND_ABILITIES_TYPES[i];
            for (final Card land : lands) {
                if (land.getType().hasSubtype(landType)) {
                    final SpellAbility sa = AbilityFactory.getAbility(mapParams, type, land);
                    sa.setBasicLandAbility(true);
                    land.getCurrentState().addManaAbility(sa);
                }
            }
        }
    } // stLandManaAbilities

    /**
     * <p>
     * Find the alternative costs to a {@link SpellAbility}.
     * </p>
     * 
     * @param sa
     *            a {@link SpellAbility}.
     * @param activator
     *            the {@link Player} for which to calculate available
     * @return a {@link List} of {@link SpellAbility} objects, each representing
     *         a possible alternative cost the provided activator can use to pay
     *         the provided {@link SpellAbility}.
     */
    public static final List<SpellAbility> getAlternativeCosts(final SpellAbility sa, final Player activator) {
        final List<SpellAbility> alternatives = new ArrayList<SpellAbility>();
        if (!sa.isBasicSpell()) {
            return alternatives;
        }

        final Card source = sa.getHostCard();
        final CardPlayOption playOption = source.mayPlay(activator);
        if (sa.isSpell() && playOption != null) {
            final PayManaCost payMana = playOption.getPayManaCost();
            if (payMana == PayManaCost.YES || payMana == PayManaCost.MAYBE) {
                final SpellAbility newSA = sa.copy();
                final SpellAbilityRestriction sar = new SpellAbilityRestriction();
                sar.setVariables(sa.getRestrictions());
                sar.setZone(null);
                newSA.setRestrictions(sar);
                alternatives.add(newSA);
            }
            if (payMana == PayManaCost.NO || payMana == PayManaCost.MAYBE) {
                final SpellAbility newSA = sa.copy();
                final SpellAbilityRestriction sar = new SpellAbilityRestriction();
                sar.setVariables(sa.getRestrictions());
                sar.setZone(null);
                newSA.setRestrictions(sar);
                newSA.setBasicSpell(false);
                newSA.setPayCosts(newSA.getPayCosts().copyWithNoMana());
                newSA.setDescription(sa.getDescription() + " (without paying its mana cost)");
                alternatives.add(newSA);
            }
        }

        for (final String keyword : source.getKeywords()) {
            if (sa.isSpell() && keyword.startsWith("Flashback")) {
                final SpellAbility flashback = sa.copy();
                flashback.setFlashBackAbility(true);
                SpellAbilityRestriction sar = new SpellAbilityRestriction();
                sar.setVariables(sa.getRestrictions());
                sar.setZone(ZoneType.Graveyard);
                flashback.setRestrictions(sar);

                // there is a flashback cost (and not the cards cost)
                if (!keyword.equals("Flashback")) {
                    flashback.setPayCosts(new Cost(keyword.substring(10), false));
                }
                alternatives.add(flashback);
            }
            if (sa.isSpell() && keyword.equals("May be played without paying its mana cost")) {
                final SpellAbility newSA = sa.copy();
                SpellAbilityRestriction sar = new SpellAbilityRestriction();
                sar.setVariables(sa.getRestrictions());
                sar.setZone(null);
                newSA.setRestrictions(sar);
                newSA.setBasicSpell(false);
                newSA.setPayCosts(newSA.getPayCosts().copyWithNoMana());
                newSA.setDescription(sa.getDescription() + " (without paying its mana cost)");
                alternatives.add(newSA);
            }
            if (sa.isSpell() && keyword
                    .startsWith("May be played without paying its mana cost and as though it has flash")) {
                final SpellAbility newSA = sa.copy();
                SpellAbilityRestriction sar = new SpellAbilityRestriction();
                sar.setVariables(sa.getRestrictions());
                sar.setInstantSpeed(true);
                newSA.setRestrictions(sar);
                newSA.setBasicSpell(false);
                newSA.setPayCosts(newSA.getPayCosts().copyWithNoMana());
                newSA.setDescription(
                        sa.getDescription() + " (without paying its mana cost and as though it has flash)");
                alternatives.add(newSA);
            }
            if (sa.isSpell() && keyword.startsWith("Alternative Cost")) {
                final SpellAbility newSA = sa.copy();
                newSA.setBasicSpell(false);
                String kw = keyword;
                if (keyword.contains("ConvertedManaCost")) {
                    final String cmc = Integer.toString(sa.getHostCard().getCMC());
                    kw = keyword.replace("ConvertedManaCost", cmc);
                }
                final Cost cost = new Cost(kw.substring(17), false).add(newSA.getPayCosts().copyWithNoMana());
                newSA.setPayCosts(cost);
                newSA.setDescription(sa.getDescription() + " (by paying " + cost.toSimpleString()
                        + " instead of its mana cost)");
                alternatives.add(newSA);
            }
            if (sa.isSpell() && keyword
                    .equals("You may cast CARDNAME as though it had flash if you pay 2 more to cast it.")) {
                final SpellAbility newSA = sa.copy();
                newSA.setBasicSpell(false);
                ManaCostBeingPaid newCost = new ManaCostBeingPaid(source.getManaCost());
                newCost.increaseColorlessMana(2);
                final Cost actualcost = new Cost(newCost.toManaCost(), false);
                newSA.setPayCosts(actualcost);
                SpellAbilityRestriction sar = new SpellAbilityRestriction();
                sar.setVariables(sa.getRestrictions());
                sar.setInstantSpeed(true);
                newSA.setRestrictions(sar);
                newSA.setDescription(sa.getDescription() + " (by paying " + actualcost.toSimpleString()
                        + " instead of its mana cost)");
                alternatives.add(newSA);
            }
            if (sa.isSpell() && keyword.endsWith(" offering")) {
                final String offeringType = keyword.split(" ")[0];
                List<Card> canOffer = CardLists.filter(
                        sa.getHostCard().getController().getCardsIn(ZoneType.Battlefield),
                        CardPredicates.isType(offeringType));
                if (source.getController()
                        .hasKeyword("You can't sacrifice creatures to cast spells or activate abilities.")) {
                    canOffer = CardLists.getNotType(canOffer, "Creature");
                }
                if (!canOffer.isEmpty()) {
                    final SpellAbility newSA = sa.copy();
                    SpellAbilityRestriction sar = new SpellAbilityRestriction();
                    sar.setVariables(sa.getRestrictions());
                    sar.setInstantSpeed(true);
                    newSA.setRestrictions(sar);
                    newSA.setBasicSpell(false);
                    newSA.setIsOffering(true);
                    newSA.setPayCosts(sa.getPayCosts());
                    newSA.setDescription(sa.getDescription() + " (" + offeringType + " offering)");
                    alternatives.add(newSA);
                }
            }
            if (sa.hasParam("Equip") && sa instanceof AbilityActivated && keyword.equals("EquipInstantSpeed")) {
                final SpellAbility newSA = ((AbilityActivated) sa).getCopy();
                SpellAbilityRestriction sar = new SpellAbilityRestriction();
                sar.setVariables(sa.getRestrictions());
                sar.setSorcerySpeed(false);
                sar.setInstantSpeed(true);
                newSA.setRestrictions(sar);
                newSA.setDescription(
                        sa.getDescription() + " (you may activate any time you could cast an instant )");
                alternatives.add(newSA);
            }
        }
        return alternatives;
    }

    /**
     * get optional additional costs.
     * 
     * @param original
     *            the original sa
     * @return an ArrayList<SpellAbility>.
     */
    public static List<SpellAbility> getOptionalCosts(final SpellAbility original) {
        final List<SpellAbility> abilities = new ArrayList<SpellAbility>();

        final Card source = original.getHostCard();
        abilities.add(original);
        if (!original.isSpell()) {
            return abilities;
        }

        // Buyback, Kicker
        for (String keyword : source.getKeywords()) {
            if (keyword.startsWith("AlternateAdditionalCost")) {
                final List<SpellAbility> newAbilities = new ArrayList<SpellAbility>();
                String[] costs = TextUtil.split(keyword, ':');
                for (SpellAbility sa : abilities) {
                    final SpellAbility newSA = sa.copy();
                    newSA.setBasicSpell(false);

                    final Cost cost1 = new Cost(costs[1], false);
                    newSA.setDescription(sa.getDescription() + " (Additional cost " + cost1.toSimpleString() + ")");
                    newSA.setPayCosts(cost1.add(sa.getPayCosts()));
                    if (newSA.canPlay()) {
                        newAbilities.add(newSA);
                    }

                    //second option
                    final SpellAbility newSA2 = sa.copy();
                    newSA2.setBasicSpell(false);

                    final Cost cost2 = new Cost(costs[2], false);
                    newSA2.setDescription(
                            sa.getDescription() + " (Additional cost " + cost2.toSimpleString() + ")");
                    newSA2.setPayCosts(cost2.add(sa.getPayCosts()));
                    if (newSA2.canPlay()) {
                        newAbilities.add(newAbilities.size(), newSA2);
                    }
                }
                abilities.clear();
                abilities.addAll(newAbilities);
            } else if (keyword.startsWith("Buyback")) {
                for (int i = 0; i < abilities.size(); i++) {
                    final SpellAbility newSA = abilities.get(i).copy();
                    newSA.setBasicSpell(false);
                    newSA.setPayCosts(new Cost(keyword.substring(8), false).add(newSA.getPayCosts()));
                    newSA.setDescription(newSA.getDescription() + " (with Buyback)");
                    newSA.addOptionalCost(OptionalCost.Buyback);
                    if (newSA.canPlay()) {
                        abilities.add(i, newSA);
                        i++;
                    }
                }
            } else if (keyword.startsWith("Entwine")) {
                for (int i = 0; i < abilities.size(); i++) {
                    final SpellAbility newSA = abilities.get(i).copy();
                    SpellAbility entwine = AbilityFactory.buildEntwineAbility(newSA);
                    entwine.setPayCosts(new Cost(keyword.substring(8), false).add(newSA.getPayCosts()));
                    entwine.addOptionalCost(OptionalCost.Entwine);
                    if (newSA.canPlay()) {
                        abilities.add(i, entwine);
                        i++;
                    }
                }
            } else if (keyword.startsWith("Kicker")) {
                for (int i = 0; i < abilities.size(); i++) {
                    String[] sCosts = TextUtil.split(keyword.substring(7), ':');
                    boolean generic = sCosts[sCosts.length - 1].trim().equals("Generic");
                    int iUnKicked = i;
                    // If this is a "generic kicker" (Undergrowth), ignore value for kicker creations
                    int numKickers = sCosts.length - (generic ? 1 : 0);
                    for (int j = 0; j < numKickers; j++) {
                        final SpellAbility newSA = abilities.get(iUnKicked).copy();
                        newSA.setBasicSpell(false);
                        final Cost cost = new Cost(sCosts[j], false);
                        newSA.setPayCosts(cost.add(newSA.getPayCosts()));
                        if (!generic) {
                            newSA.setDescription(
                                    newSA.getDescription() + " (Kicker " + cost.toSimpleString() + ")");
                            newSA.addOptionalCost(j == 0 ? OptionalCost.Kicker1 : OptionalCost.Kicker2);
                        } else {
                            newSA.setDescription(
                                    newSA.getDescription() + " (Optional " + cost.toSimpleString() + ")");
                            newSA.addOptionalCost(OptionalCost.Generic);
                        }
                        if (newSA.canPlay()) {
                            abilities.add(i, newSA);
                            i++;
                            iUnKicked++;
                        }
                    }
                    if (numKickers == 2) { // case for both kickers - it's hardcoded since they never have more than 2 kickers
                        final SpellAbility newSA = abilities.get(iUnKicked).copy();
                        newSA.setBasicSpell(false);
                        final Cost cost1 = new Cost(sCosts[0], false);
                        final Cost cost2 = new Cost(sCosts[1], false);
                        newSA.setDescription(newSA.getDescription() + String.format(" (Both kickers: %s and %s)",
                                cost1.toSimpleString(), cost2.toSimpleString()));
                        newSA.setPayCosts(cost2.add(cost1.add(newSA.getPayCosts())));
                        newSA.addOptionalCost(OptionalCost.Kicker1);
                        newSA.addOptionalCost(OptionalCost.Kicker2);
                        if (newSA.canPlay()) {
                            abilities.add(i, newSA);
                            i++;
                        }
                    }
                }
            }
        }

        if (source.hasKeyword("Conspire")) {
            int amount = source.getAmountOfKeyword("Conspire");
            for (int kwInstance = 1; kwInstance <= amount; kwInstance++) {
                for (int i = 0; i < abilities.size(); i++) {
                    final SpellAbility newSA = abilities.get(i).copy();
                    newSA.setBasicSpell(false);
                    final String conspireCost = "tapXType<2/Creature.SharesColorWith/untapped creature you control that shares a color with "
                            + source.getName() + ">";
                    newSA.setPayCosts(new Cost(conspireCost, false).add(newSA.getPayCosts()));
                    final String tag = kwInstance > 1 ? " (Conspire " + kwInstance + ")" : " (Conspire)";
                    newSA.setDescription(newSA.getDescription() + tag);
                    newSA.addOptionalCost(OptionalCost.Conspire);
                    newSA.addConspireInstance();
                    if (newSA.canPlay()) {
                        abilities.add(++i, newSA);
                    }
                }
            }
        }

        // Splice
        final List<SpellAbility> newAbilities = new ArrayList<SpellAbility>();
        for (SpellAbility sa : abilities) {
            if (sa.isSpell() && sa.getHostCard().getType().hasStringType("Arcane") && sa.getApi() != null) {
                newAbilities.addAll(GameActionUtil.getSpliceAbilities(sa));
            }
        }
        abilities.addAll(newAbilities);
        return abilities;
    }

    /**
     * <p>
     * getSpliceAbilities.
     * </p>
     * 
     * @param sa
     *            a SpellAbility.
     * @return an ArrayList<SpellAbility>.
     * get abilities with all Splice options
     */
    private static final List<SpellAbility> getSpliceAbilities(SpellAbility sa) {
        List<SpellAbility> newSAs = new ArrayList<SpellAbility>();
        List<SpellAbility> allSaCombinations = new ArrayList<SpellAbility>();
        allSaCombinations.add(sa);
        Card source = sa.getHostCard();

        for (Card c : sa.getActivatingPlayer().getCardsIn(ZoneType.Hand)) {
            if (c.equals(source)) {
                continue;
            }

            String spliceKwCost = null;
            for (String keyword : c.getKeywords()) {
                if (keyword.startsWith("Splice")) {
                    spliceKwCost = keyword.substring(19);
                    break;
                }
            }

            if (spliceKwCost == null)
                continue;

            Map<String, String> params = AbilityFactory.getMapParams(c.getCurrentState().getFirstUnparsedAbility());
            AbilityRecordType rc = AbilityRecordType.getRecordType(params);
            ApiType api = rc.getApiTypeOf(params);
            AbilitySub subAbility = (AbilitySub) AbilityFactory.getAbility(AbilityRecordType.SubAbility, api,
                    params, null, c);

            // Add the subability to all existing variants
            for (int i = 0; i < allSaCombinations.size(); ++i) {
                //create a new spell copy
                final SpellAbility newSA = allSaCombinations.get(i).copy();
                newSA.setBasicSpell(false);
                newSA.setPayCosts(new Cost(spliceKwCost, false).add(newSA.getPayCosts()));
                newSA.setDescription(newSA.getDescription() + " (Splicing " + c + " onto it)");
                newSA.addSplicedCards(c);

                // copy all subAbilities
                SpellAbility child = newSA;
                while (child.getSubAbility() != null) {
                    AbilitySub newChild = child.getSubAbility().getCopy();
                    child.setSubAbility(newChild);
                    child.setActivatingPlayer(newSA.getActivatingPlayer());
                    child = newChild;
                }

                //add the spliced ability to the end of the chain
                child.setSubAbility(subAbility);

                //set correct source and activating player to all the spliced abilities
                child = subAbility;
                while (child != null) {
                    child.setHostCard(source);
                    child.setActivatingPlayer(newSA.getActivatingPlayer());
                    child = child.getSubAbility();
                }
                newSAs.add(newSA);
                allSaCombinations.add(++i, newSA);
            }
        }
        return newSAs;
    }

    private static boolean hasUrzaLands(final Player p) {
        final CardCollectionView landsControlled = p.getCardsIn(ZoneType.Battlefield);
        return Iterables.any(landsControlled,
                Predicates.and(CardPredicates.isType("Urza's"), CardPredicates.isType("Mine")))
                && Iterables.any(landsControlled,
                        Predicates.and(CardPredicates.isType("Urza's"), CardPredicates.isType("Power-Plant")))
                && Iterables.any(landsControlled,
                        Predicates.and(CardPredicates.isType("Urza's"), CardPredicates.isType("Tower")));
    }

    public static String generatedMana(final SpellAbility sa) {
        // Calculate generated mana here for stack description and resolving

        int amount = sa.hasParam("Amount")
                ? AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa)
                : 1;

        AbilityManaPart abMana = sa.getManaPart();
        String baseMana;
        if (abMana.isComboMana()) {
            baseMana = abMana.getExpressChoice();
            if (baseMana.isEmpty()) {
                baseMana = abMana.getOrigProduced();
            }
        } else if (abMana.isAnyMana()) {
            baseMana = abMana.getExpressChoice();
            if (baseMana.isEmpty()) {
                baseMana = "Any";
            }
        } else if (sa.getApi() == ApiType.ManaReflected) {
            baseMana = abMana.getExpressChoice();
        } else if (abMana.isSpecialMana()) {
            baseMana = abMana.getExpressChoice();
        } else {
            baseMana = abMana.mana();
        }

        if (sa.hasParam("Bonus")) {
            // For mana abilities that get a bonus
            // Bonus currently MULTIPLIES the base amount. Base Amounts should
            // ALWAYS be Base
            int bonus = 0;
            if (sa.getParam("Bonus").equals("UrzaLands")) {
                if (hasUrzaLands(sa.getActivatingPlayer())) {
                    bonus = Integer.parseInt(sa.getParam("BonusProduced"));
                }
            }

            amount += bonus;
        }

        if (sa.getSubAbility() != null) {
            // Mark SAs with subAbilities as undoable. These are generally things like damage, and other stuff
            // that's hard to track and remove
            sa.setUndoable(false);
        } else {
            try {
                if ((sa.getParam("Amount") != null) && (amount != Integer.parseInt(sa.getParam("Amount")))) {
                    sa.setUndoable(false);
                }
            } catch (final NumberFormatException n) {
                sa.setUndoable(false);
            }
        }

        final StringBuilder sb = new StringBuilder();
        if (amount == 0) {
            sb.append("0");
        } else if (abMana.isComboMana()) {
            // amount is already taken care of in resolve method for combination mana, just append baseMana
            sb.append(baseMana);
        } else {
            if (StringUtils.isNumeric(baseMana)) {
                sb.append(amount * Integer.parseInt(baseMana));
            } else {
                sb.append(baseMana);
                for (int i = 1; i < amount; i++) {
                    sb.append(" ").append(baseMana);
                }
            }
        }
        return sb.toString();
    }
} // end class GameActionUtil