/* * BonusManager * Copyright 2009 (c) Tom Parker <thpr@users.sourceforge.net> * derived from PlayerCharacter.java * Copyright 2001 (C) Bryan McRoberts <merton_monk@yahoo.com> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package pcgen.core; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import pcgen.base.formula.Formula; import pcgen.base.util.WrappedMapSet; import pcgen.cdom.base.BonusContainer; import pcgen.cdom.base.CDOMObject; import pcgen.cdom.base.Constants; import pcgen.cdom.base.FormulaFactory; import pcgen.cdom.enumeration.StringKey; import pcgen.core.analysis.ChooseActivation; import pcgen.core.bonus.BonusObj; import pcgen.core.bonus.BonusPair; import pcgen.core.bonus.util.MissingObject; import pcgen.core.display.BonusDisplay; import pcgen.core.prereq.Prerequisite; import pcgen.core.utils.CoreUtility; import pcgen.util.Delta; import pcgen.util.Logging; public class BonusManager { /** %LIST - Replace one value selected into this spot */ private static final String VALUE_TOKEN_REPLACEMENT = "%LIST"; //$NON-NLS-1$ /** LIST - Replace all the values selected into this spot */ private static final String LIST_TOKEN_REPLACEMENT = "LIST"; //$NON-NLS-1$ private static final String VALUE_TOKEN_PATTERN = Pattern .quote(VALUE_TOKEN_REPLACEMENT); private static final String VAR_TOKEN_REPLACEMENT = "%VAR"; //$NON-NLS-1$ private static final String VAR_TOKEN_PATTERN = Pattern .quote(VAR_TOKEN_REPLACEMENT); private static final List<String> NO_ASSOC_LIST = Collections .singletonList(""); private Map<String, String> activeBonusMap = new ConcurrentHashMap<>(); private Map<String, Double> cachedActiveBonusSumsMap = new ConcurrentHashMap<>(); private Map<BonusObj, Object> activeBonusBySource = new IdentityHashMap<>(); private Map<BonusObj, TempBonusInfo> tempBonusBySource = new IdentityHashMap<>(); private Set<String> tempBonusFilters = new TreeSet<>(); private final PlayerCharacter pc; private Map<String, String> checkpointMap; public BonusManager(PlayerCharacter p) { pc = p; } /** * @param fullyQualifiedBonusType * @return Total bonus for prefix from the activeBonus HashMap */ private double sumActiveBonusMap(String fullyQualifiedBonusType) { double bonus = 0; if (fullyQualifiedBonusType == null) { Logging.errorPrint("Unable to sum BONUS when request is null"); return bonus; } fullyQualifiedBonusType = fullyQualifiedBonusType.toUpperCase(); if (cachedActiveBonusSumsMap.containsKey(fullyQualifiedBonusType)) { return cachedActiveBonusSumsMap.get(fullyQualifiedBonusType); } final List<String> aList = new ArrayList<>(); for (String fullyQualifedCurrentBonus : activeBonusMap.keySet()) { // aKey is either of the form: // COMBAT.AC // or // COMBAT.AC:Luck // or // COMBAT.AC:Armor.REPLACE if (aList.contains(fullyQualifedCurrentBonus)) { continue; } if (fullyQualifedCurrentBonus == null) { Logging.errorPrint("null BONUS: loaded into activeBonusMap. " + fullyQualifiedBonusType + " was requested"); continue; } String currentTypedBonusNameInfo = fullyQualifedCurrentBonus; // rString could be something like: // COMBAT.AC:Armor.REPLACE // So need to remove the .STACK or .REPLACE // to get a match for prefix like: COMBAT.AC:Armor if (currentTypedBonusNameInfo.endsWith(".STACK")) { currentTypedBonusNameInfo = currentTypedBonusNameInfo.substring(0, currentTypedBonusNameInfo.length() - 6); } else if (currentTypedBonusNameInfo.endsWith(".REPLACE")) { currentTypedBonusNameInfo = currentTypedBonusNameInfo.substring(0, currentTypedBonusNameInfo.length() - 8); } if (currentTypedBonusNameInfo == null) { Logging.errorPrint("Unable to process BONUS: " + fullyQualifedCurrentBonus + " when " + fullyQualifiedBonusType + " was requested"); continue; } // if prefix is of the form: // COMBAT.AC // then is must match rstring: // COMBAT.AC // COMBAT.AC:Luck // COMBAT.AC:Armor.REPLACE // However, it must not match // COMBAT.ACCHECK if ((currentTypedBonusNameInfo.length() > fullyQualifiedBonusType.length()) && !currentTypedBonusNameInfo.startsWith(fullyQualifiedBonusType + ":")) { continue; } if (currentTypedBonusNameInfo.startsWith(fullyQualifiedBonusType)) { aList.add(currentTypedBonusNameInfo); aList.add(currentTypedBonusNameInfo + ".STACK"); aList.add(currentTypedBonusNameInfo + ".REPLACE"); final double aBonus = getActiveBonusForMapKey(currentTypedBonusNameInfo, Double.NaN); final double replaceBonus = getActiveBonusForMapKey(currentTypedBonusNameInfo + ".REPLACE", Double.NaN); final double stackBonus = getActiveBonusForMapKey(currentTypedBonusNameInfo + ".STACK", 0); // // Using NaNs in order to be able to get the max // between an undefined bonus and a negative // if (Double.isNaN(aBonus)) // no bonusKey { if (!Double.isNaN(replaceBonus)) { // no bonusKey, but there // is a replaceKey bonus += replaceBonus; } } else if (Double.isNaN(replaceBonus)) { // is a bonusKey and no replaceKey bonus += aBonus; } else { // is a bonusKey and a replaceKey bonus += Math.max(aBonus, replaceBonus); } // always add stackBonus bonus += stackBonus; } } cachedActiveBonusSumsMap.put(fullyQualifiedBonusType, bonus); return bonus; } /** * Searches the activeBonus HashMap for aKey * * @param fullyQualifiedBonusType * @param defaultValue * * @return defaultValue if aKey not found */ private double getActiveBonusForMapKey(String fullyQualifiedBonusType, final double defaultValue) { fullyQualifiedBonusType = fullyQualifiedBonusType.toUpperCase(); final String regVal = activeBonusMap.get(fullyQualifiedBonusType); if (regVal != null) { return Double.parseDouble(regVal); } return defaultValue; } public double getBonusDueToType(String bonusName, String bonusInfo, String bonusType) { final String typeString = bonusName + "." + bonusInfo + ":" + bonusType; return sumActiveBonusMap(typeString); } public double getTotalBonusTo(String bonusName, String bonusInfo) { final String prefix = new StringBuilder(bonusName).append('.').append( bonusInfo).toString(); return sumActiveBonusMap(prefix); } public String getSpellBonusType(String bonusName, String bonusInfo) { String prefix = new StringBuilder(bonusName).append('.').append( bonusInfo).toString(); prefix = prefix.toUpperCase(); for (String fullyQualifedBonusType : activeBonusMap.keySet()) { String typedBonusNameInfo = fullyQualifedBonusType; // rString could be something like: // COMBAT.AC:Armor.REPLACE // So need to remove the .STACK or .REPLACE // to get a match for prefix like: COMBAT.AC:Armor if (fullyQualifedBonusType.endsWith(".STACK")) { typedBonusNameInfo = fullyQualifedBonusType.substring(0, fullyQualifedBonusType.length() - 6); } else if (fullyQualifedBonusType.endsWith(".REPLACE")) { typedBonusNameInfo = fullyQualifedBonusType.substring(0, fullyQualifedBonusType.length() - 8); } // if prefix is of the form: // COMBAT.AC // then it must match // COMBAT.AC // COMBAT.AC:Luck // COMBAT.AC:Armor.REPLACE // However, it must not match // COMBAT.ACCHECK if ((typedBonusNameInfo.length() > prefix.length()) && !typedBonusNameInfo.startsWith(prefix + ":")) { continue; } if (typedBonusNameInfo.startsWith(prefix)) { final int typeIndex = typedBonusNameInfo.indexOf(":"); if (typeIndex > 0) { return (fullyQualifedBonusType.substring(typeIndex + 1)); // use aKey to get // .REPLACE or // .STACK } return Constants.EMPTY_STRING; // no type; } } return Constants.EMPTY_STRING; // just return no type } /** * Build the bonus HashMap from all active BonusObj's */ void buildActiveBonusMap() { activeBonusMap = new ConcurrentHashMap<>(); cachedActiveBonusSumsMap = new ConcurrentHashMap<>(); Map<String, String> nonStackMap = new ConcurrentHashMap<>(); Map<String, String> stackMap = new ConcurrentHashMap<>(); Set<BonusObj> processedBonuses = new WrappedMapSet<>( IdentityHashMap.class); //Logging.log(Logging.INFO, "=== Start bonus processing."); // // We do a first pass of just the "static" bonuses // as they require less computation and no recursion List<BonusObj> bonusListCopy = new ArrayList<>(); bonusListCopy.addAll(getActiveBonusList()); for (BonusObj bonus : bonusListCopy) { if (!bonus.isValueStatic()) { continue; } final Object source = getSourceObject(bonus); if (source == null) { if (Logging.isDebugMode()) { Logging.debugPrint("BONUS: " + bonus + " ignored due to no creator"); } continue; } // Keep track of which bonuses have been calculated //Logging.log(Logging.INFO, "Processing bonus " + bonus + " - static."); processedBonuses.add(bonus); for (BonusPair bp : getStringListFromBonus(bonus)) { final double iBonus = bp.resolve(pc).doubleValue(); setActiveBonusStack(iBonus, bp.fullyQualifiedBonusType, nonStackMap, stackMap); totalBonusesForType(nonStackMap, stackMap, bp.fullyQualifiedBonusType, activeBonusMap); if (Logging.isDebugMode()) { String id; if (source instanceof CDOMObject) { id = ((CDOMObject)source).getDisplayName(); } else { id = source.toString(); } Logging.debugPrint("BONUS: " + id + " : " + iBonus + " : " + bp.fullyQualifiedBonusType); } } } // // Now we do all the BonusObj's that require calculations bonusListCopy = new ArrayList<>(); bonusListCopy.addAll(getActiveBonusList()); for (BonusObj bonus : getActiveBonusList()) { if (processedBonuses.contains(bonus)) { continue; } final CDOMObject anObj = (CDOMObject) getSourceObject(bonus); if (anObj == null) { continue; } try { processBonus(bonus, new WrappedMapSet<>( IdentityHashMap.class), processedBonuses, nonStackMap, stackMap); } catch (Exception e) { Logging.errorPrint(e.getLocalizedMessage(), e); continue; } } } /** * Combines the non-stacking bonus maximum and stacking * bonus totals to a total bonus for the bonus type. * * @param nonStackMap * The map of non-stacking (i.e. highest wins) bonuses being built up. * @param stackMap * The map of stacking (i.e. total all) bonuses being built up. * @param fullyQualifiedBonusType * The type of the bonus e.g. STAT.DEX:LUCK * @param targetMap * The map of bonuses (stack+non-stack) being built up which will be populated with the total bonus. */ private void totalBonusesForType(Map<String, String> nonStackMap, Map<String, String> stackMap, String fullyQualifiedBonusType, Map<String, String> targetMap) { if (fullyQualifiedBonusType != null) { fullyQualifiedBonusType = fullyQualifiedBonusType.toUpperCase(); } String nonStackString = nonStackMap.get(fullyQualifiedBonusType); Float nonStackVal = nonStackString == null ? 0.0f : Float.valueOf(nonStackString); String stackString = stackMap.get(fullyQualifiedBonusType); Float stackVal = stackString == null ? 0.0f : Float.valueOf(stackString); Float FullValue = nonStackVal + stackVal; putActiveBonusMap(fullyQualifiedBonusType, String.valueOf(FullValue), targetMap); } public Collection<BonusObj> getActiveBonusList() { return activeBonusBySource.keySet(); } public void setActiveBonusList() { activeBonusBySource = getAllActiveBonuses(); } public String listBonusesFor(String bonusName, String bonusInfo) { final String prefix = new StringBuilder(bonusName).append('.').append( bonusInfo).toString(); final StringBuilder buf = new StringBuilder(); final List<String> aList = new ArrayList<>(); // final List<TypedBonus> bonuses = theBonusMap.get(prefix); // if ( bonuses == null ) // { // return Constants.EMPTY_STRING; // } // final List<String> bonusStrings = // TypedBonus.totalBonusesByType(bonuses); // return CoreUtility.commaDelimit(bonusStrings); final Set<String> keys = new TreeSet<>(); for (String fullyQualifiedBonusType : activeBonusMap.keySet()) { if (fullyQualifiedBonusType.startsWith(prefix)) { keys.add(fullyQualifiedBonusType); } } for (String fullyQualifiedBonusType : keys) { // make a list of keys that end with .REPLACE if (fullyQualifiedBonusType.endsWith(".REPLACE")) { aList.add(fullyQualifiedBonusType); } else { String reason = ""; if (fullyQualifiedBonusType.length() > prefix.length()) { reason = fullyQualifiedBonusType.substring(prefix.length() + 1); } final int b = (int) getActiveBonusForMapKey(fullyQualifiedBonusType, 0); if (b == 0) { continue; } if (!"NULL".equals(reason) && (!reason.isEmpty())) { if (buf.length() > 0) { buf.append(", "); } buf.append(reason).append(' '); } buf.append(Delta.toString(b)); } } // Now adjust the bonus if the .REPLACE value // replaces the value without .REPLACE for (String fullyQualifiedBonusType_Replace : aList) { if (fullyQualifiedBonusType_Replace.length() > 7) { final String aKey = fullyQualifiedBonusType_Replace.substring(0, fullyQualifiedBonusType_Replace.length() - 8); final double replaceBonus = getActiveBonusForMapKey(fullyQualifiedBonusType_Replace, 0); double aBonus = getActiveBonusForMapKey(aKey, 0); aBonus += getActiveBonusForMapKey(aKey + ".STACK", 0); final int b = (int) Math.max(aBonus, replaceBonus); if (b == 0) { continue; } if (buf.length() > 0) { buf.append(", "); } final String reason = aKey.substring(prefix.length() + 1); if (!"NULL".equals(reason)) { buf.append(reason).append(' '); } buf.append(Delta.toString(b)); } } return buf.toString(); } /** * - Get's a list of dependencies from aBonus - Finds all active bonuses * that add to those dependencies and have not been processed and * recursively calls itself - Once recursed in, it adds the computed bonus * to activeBonusMap * * @param aBonus * The bonus to be processed. * @param prevProcessed * The list of bonuses which have already been processed in this * stack of calls to processBonus. * @param processedBonuses * The list of bonuses which have already been processed overall. * @param nonStackMap * The map of non-stacking (i.e. highest wins) bonuses being built up. * @param stackMap * The map of stacking (i.e. total all) bonuses being built up. */ private void processBonus(final BonusObj aBonus, final Set<BonusObj> prevProcessed, Set<BonusObj> processedBonuses, Map<String, String> nonStackMap, Map<String, String> stackMap) { // Make sure we don't get into an infinite loop - can occur due to LST // coding or best guess dependancy mapping if (prevProcessed.contains(aBonus)) { if (Logging.isDebugMode()) { Logging .log( Logging.DEBUG, "Ignoring bonus loop for " //$NON-NLS-1$ + aBonus + " as it was already processed. Bonuses already processed: " //$NON-NLS-1$ + prevProcessed); Logging.log(Logging.DEBUG, " Depend map is " + aBonus.listDependsMap()); //$NON-NLS-1$ } return; } prevProcessed.add(aBonus); final List<BonusObj> aList = new ArrayList<>(); // Go through all bonuses and check to see if they add to // aBonus's dependencies and have not already been processed for (BonusObj newBonus : getActiveBonusList()) { if (processedBonuses.contains(newBonus)) { continue; } if (aBonus.getDependsOn(newBonus.getUnparsedBonusInfoList()) || aBonus.getDependsOnBonusName(newBonus.getBonusName())) { aList.add(newBonus); } } // go through all the BonusObj's that aBonus depends on // and process them first for (BonusObj newBonus : aList) { // Recursively call itself processBonus(newBonus, prevProcessed, processedBonuses, nonStackMap, stackMap); } // Double check that it hasn't been processed yet if (processedBonuses.contains(aBonus)) { return; } // Add to processed list //Logging.log(Logging.INFO, "Processing bonus " + aBonus + " depends on " + aBonus.listDependsMap()); processedBonuses.add(aBonus); final CDOMObject anObj = (CDOMObject) getSourceObject(aBonus); if (anObj == null) { prevProcessed.remove(aBonus); return; } // calculate bonus and add to activeBonusMap for (BonusPair bp : getStringListFromBonus(aBonus)) { final double iBonus = bp.resolve(pc).doubleValue(); setActiveBonusStack(iBonus, bp.fullyQualifiedBonusType, nonStackMap, stackMap); totalBonusesForType(nonStackMap, stackMap, bp.fullyQualifiedBonusType, activeBonusMap); // Logging.debugPrint("vBONUS: " + anObj.getDisplayName() + " : " // + iBonus + " : " + bp.fullyQualifiedBonusType); } prevProcessed.remove(aBonus); } /** * Figures out if a bonus should stack based on type, then adds it to the * supplied map. * * @param bonus * The value of the bonus. * @param fullyQualifiedBonusType * The type of the bonus e.g. STAT.DEX:LUCK * @param nonStackbonusMap * The map of non-stacking (i.e. highest wins) bonuses being built up. * @param stackingBonusMap * The map of stacking (i.e. total all) bonuses being built up. */ private void setActiveBonusStack(double bonus, String fullyQualifiedBonusType, Map<String, String> nonStackbonusMap, Map<String, String> stackingBonusMap) { if (fullyQualifiedBonusType != null) { fullyQualifiedBonusType = fullyQualifiedBonusType.toUpperCase(); // only specific bonuses can actually be fractional // -> TODO should define this in external file if (!fullyQualifiedBonusType.startsWith("ITEMWEIGHT") && !fullyQualifiedBonusType.startsWith("ITEMCOST") && !fullyQualifiedBonusType.startsWith("ACVALUE") && !fullyQualifiedBonusType.startsWith("ITEMCAPACITY") && !fullyQualifiedBonusType.startsWith("LOADMULT") && !fullyQualifiedBonusType.startsWith("FEAT") && (fullyQualifiedBonusType.indexOf("DAMAGEMULT") < 0)) { bonus = ((int) bonus); // TODO: never used } } else { return; } // default to non-stacking bonuses int index = -1; // bonusType is either of form: // COMBAT.AC // or // COMBAT.AC:Luck // or // COMBAT.AC:Armor.REPLACE // final StringTokenizer aTok = new StringTokenizer(fullyQualifiedBonusType, ":"); if (aTok.countTokens() == 2) { // need 2nd token to see if it should stack final String aString; aTok.nextToken(); aString = aTok.nextToken(); if (aString != null) { index = SettingsHandler.getGame() .getUnmodifiableBonusStackList().indexOf(aString); // e.g. // Dodge } } else { // un-named (or un-TYPE'd) bonuses stack index = 1; } // .STACK means stack with everything // .REPLACE means stack with other .REPLACE if (fullyQualifiedBonusType.endsWith(".STACK") || fullyQualifiedBonusType.endsWith(".REPLACE")) { index = 1; } // If it's a negative bonus, it always needs to be added if (bonus < 0) { index = 1; } if (index == -1) // a non-stacking bonus { final String aVal = nonStackbonusMap.get(fullyQualifiedBonusType); if (aVal == null) { putActiveBonusMap(fullyQualifiedBonusType, String.valueOf(bonus), nonStackbonusMap); } else { float existingBonus = Float.parseFloat(aVal); putActiveBonusMap(fullyQualifiedBonusType, String.valueOf(Math.max(bonus, existingBonus)), nonStackbonusMap); } } else // a stacking bonus { final String aVal = stackingBonusMap.get(fullyQualifiedBonusType); if (aVal == null) { putActiveBonusMap(fullyQualifiedBonusType, String.valueOf(bonus), stackingBonusMap); } else { putActiveBonusMap(fullyQualifiedBonusType, String.valueOf(bonus + Float.parseFloat(aVal)), stackingBonusMap); } } } /** * Put the provided bonus key and value into the supplied bonus map. Some * sanity checking is done on the key. * * @param fullyQualifiedBonusType * The bonus key * @param bonusValue * The value of the bonus * @param bonusMap * The map of bonuses being built. */ private void putActiveBonusMap(final String fullyQualifiedBonusType, final String bonusValue, Map<String, String> bonusMap) { // // This is a bad idea...will add whatever the bonus is to ALL skills // if (fullyQualifiedBonusType.equalsIgnoreCase("SKILL.LIST")) { return; } bonusMap.put(fullyQualifiedBonusType, bonusValue); } public int getPartialStatBonusFor(PCStat stat, boolean useTemp, boolean useEquip) { String statAbbr = stat.getKeyName(); final String prefix = "STAT." + statAbbr; Map<String, String> bonusMap = new HashMap<>(); Map<String, String> nonStackMap = new ConcurrentHashMap<>(); Map<String, String> stackMap = new ConcurrentHashMap<>(); for (BonusObj bonus : getActiveBonusList()) { if (pc.isApplied(bonus) && bonus.getBonusName().equals("STAT")) { boolean found = false; Object co = getSourceObject(bonus); for (Object element : bonus.getBonusInfoList()) { if (element instanceof PCStat && element.equals(stat)) { found = true; break; } // TODO: This should be put into a proper object when // parisng. if (element instanceof MissingObject) { String name = ((MissingObject) element).getObjectName(); if (("%LIST".equals(name) || "LIST".equals(name)) && co instanceof CDOMObject) { CDOMObject creator = (CDOMObject) co; for (String assoc : pc .getConsolidatedAssociationList(creator)) { //TODO Case sensitivity? if (assoc.contains(statAbbr)) { found = true; break; } } } } } if (!found) { continue; } // The bonus has been applied to the target stat // Should it be included? boolean addIt = false; if (co instanceof Equipment || co instanceof EquipmentModifier) { addIt = useEquip; } else if (co instanceof Ability) { List<String> types = ((Ability)co).getTypes(); if (types.contains("Equipment")) { addIt = useEquip; } else { addIt = true; } } else if (tempBonusBySource.containsKey(bonus)) { addIt = useTemp; } else { addIt = true; } if (addIt) { // Grab the list of relevant types so that we can build up // the // bonuses with the stacking rules applied. for (BonusPair bp : getStringListFromBonus(bonus)) { if (bp.fullyQualifiedBonusType.startsWith(prefix)) { setActiveBonusStack(bp.resolve(pc).doubleValue(), bp.fullyQualifiedBonusType, nonStackMap, stackMap); totalBonusesForType(nonStackMap, stackMap, bp.fullyQualifiedBonusType, bonusMap); } } } } } // Sum the included bonuses to the stat to get our result. int total = 0; for (String bKey : bonusMap.keySet()) { total += Float.parseFloat(bonusMap.get(bKey)); } return total; } public BonusManager buildDeepClone(PlayerCharacter apc) { BonusManager clone = new BonusManager(apc); clone.activeBonusBySource.putAll(activeBonusBySource); clone.tempBonusBySource.putAll(tempBonusBySource); clone.activeBonusMap.putAll(activeBonusMap); clone.tempBonusFilters.addAll(tempBonusFilters); return clone; } public void checkpointBonusMap() { checkpointMap = activeBonusMap; } public boolean compareToCheckpoint() { return checkpointMap != null && checkpointMap.equals(activeBonusMap); } public Map<BonusObj, TempBonusInfo> getTempBonusMap() { return new IdentityHashMap<>(tempBonusBySource); } public Map<String, String> getBonuses(String bonusName, String bonusInfo) { Map<String, String> returnMap = new HashMap<>(); String prefix = bonusName + "." + bonusInfo + "."; for (Map.Entry<String, String> entry : activeBonusMap.entrySet()) { String fullyQualifiedBonusType = entry.getKey(); if (fullyQualifiedBonusType.startsWith(prefix)) { returnMap.put(fullyQualifiedBonusType, entry.getValue()); } } return returnMap; } public TempBonusInfo addTempBonus(BonusObj bonus, Object source, Object target) { TempBonusInfo tempBonusInfo = new TempBonusInfo(source, target); tempBonusBySource.put(bonus, tempBonusInfo); return tempBonusInfo; } public void removeTempBonus(BonusObj bonus) { tempBonusBySource.remove(bonus); } public Set<String> getTempBonusDisplayNames() { final Set<String> ret = new TreeSet<>(); for (Map.Entry<BonusObj, TempBonusInfo> me : tempBonusBySource .entrySet()) { ret.add(BonusDisplay.getBonusDisplayName(me.getValue())); } return ret; } public List<BonusObj> getTempBonusList(String aCreator, String aTarget) { final List<BonusObj> aList = new ArrayList<>(); for (Map.Entry<BonusObj, TempBonusInfo> me : tempBonusBySource .entrySet()) { BonusObj bonus = me.getKey(); final Object aTO = me.getValue().target; final Object aCO = me.getValue().source; String targetName = Constants.EMPTY_STRING; String creatorName = Constants.EMPTY_STRING; if (aCO instanceof CDOMObject) { creatorName = ((CDOMObject) aCO).getKeyName(); } if (aTO instanceof PlayerCharacter) { targetName = ((PlayerCharacter) aTO).getName(); } else if (aTO instanceof CDOMObject) { targetName = ((CDOMObject) aTO).getKeyName(); } if (creatorName.equals(aCreator) && targetName.equals(aTarget)) { aList.add(bonus); } } return aList; } public List<String> getNamedTempBonusList() { final List<String> aList = new ArrayList<>(); Map<BonusObj, TempBonusInfo> filteredTempBonusList = getFilteredTempBonusList(); for (Map.Entry<BonusObj, TempBonusInfo> me : filteredTempBonusList .entrySet()) { BonusObj aBonus = me.getKey(); if (aBonus == null) { continue; } if (!pc.isApplied(aBonus)) { continue; } final CDOMObject aCreator = (CDOMObject) me.getValue().source; if (aCreator == null) { continue; } final String aName = aCreator.getKeyName(); if (!aList.contains(aName)) { aList.add(aName); } } return aList; } public List<String> getNamedTempBonusDescList() { final List<String> aList = new ArrayList<>(); Map<BonusObj, TempBonusInfo> filteredTempBonusList = getFilteredTempBonusList(); for (Map.Entry<BonusObj, TempBonusInfo> me : filteredTempBonusList .entrySet()) { BonusObj aBonus = me.getKey(); if (aBonus == null) { continue; } if (!pc.isApplied(aBonus)) { continue; } final CDOMObject aCreator = (CDOMObject) me.getValue().source; if (aCreator == null) { continue; } String aDesc = aCreator.getSafe(StringKey.DESCRIPTION); if (!aList.contains(aDesc)) { aList.add(aDesc); } } return aList; } public Map<BonusObj, TempBonusInfo> getFilteredTempBonusList() { final Map<BonusObj, TempBonusInfo> ret = new IdentityHashMap<>(); for (Map.Entry<BonusObj, TempBonusInfo> me : tempBonusBySource .entrySet()) { BonusObj bonus = me.getKey(); TempBonusInfo ti = me.getValue(); if (!tempBonusFilters.contains(BonusDisplay.getBonusDisplayName(ti))) { ret.put(bonus, ti); } } return ret; } public Set<String> getTempBonusFilters() { return tempBonusFilters; } public void addTempBonusFilter(String bonusStr) { tempBonusFilters.add(bonusStr); } public void removeTempBonusFilter(String bonusStr) { tempBonusFilters.remove(bonusStr); } public Map<BonusObj, Object> getTempBonuses() { Map<BonusObj, Object> map = new IdentityHashMap<>(); for (Map.Entry<BonusObj, TempBonusInfo> me : getFilteredTempBonusList() .entrySet()) { final BonusObj bonus = me.getKey(); pc.setApplied(bonus, false); Object source = me.getValue().source; CDOMObject cdomsource = (source instanceof CDOMObject) ? (CDOMObject) source : null; if (bonus.qualifies(pc, cdomsource)) { pc.setApplied(bonus, true); } if (pc.isApplied(bonus)) { map.put(bonus, source); } } return map; } public Map<BonusObj, TempBonusInfo> getTempBonusMap(String aCreator, String aTarget) { final Map<BonusObj, TempBonusInfo> aMap = new IdentityHashMap<>(); for (Map.Entry<BonusObj, TempBonusInfo> me : tempBonusBySource .entrySet()) { BonusObj bonus = me.getKey(); TempBonusInfo tbi = me.getValue(); final Object aTO = tbi.target; final Object aCO = tbi.source; String targetName = Constants.EMPTY_STRING; String creatorName = Constants.EMPTY_STRING; if (aCO instanceof CDOMObject) { creatorName = ((CDOMObject) aCO).getKeyName(); } if (aTO instanceof PlayerCharacter) { targetName = ((PlayerCharacter) aTO).getName(); } else if (aTO instanceof CDOMObject) { targetName = ((CDOMObject) aTO).getKeyName(); } if (creatorName.equals(aCreator) && targetName.equals(aTarget)) { aMap.put(bonus, tbi); } } return aMap; } public String getBonusContext(BonusObj bo, boolean shortForm) { final StringBuilder sb = new StringBuilder(50); boolean bEmpty = true; sb.append('['); if (bo.hasPrerequisites()) { for (Prerequisite p : bo.getPrerequisiteList()) { if (!bEmpty) { sb.append(','); } sb.append(p.getDescription(shortForm)); bEmpty = false; } } String type = bo.getTypeString(); if (!type.isEmpty()) { if (!shortForm) { if (!bEmpty) { sb.append('|'); } sb.append("TYPE="); bEmpty = false; } if (!shortForm || sb.charAt(sb.length() - 1) == '[') { sb.append(type); bEmpty = false; } } // // If there is nothing shown in between the [], then show the Bonus's // type // if (!bEmpty) { sb.append('|'); } sb.append(getSourceString(bo)); sb.append(']'); return sb.toString(); } private String getSourceString(BonusObj bo) { Object source = getSourceObject(bo); if (source == null) { return "NONE"; } if (source instanceof PlayerCharacter) { return ((PlayerCharacter) source).getName(); } else // if (source instanceof PObject) { return source.toString(); } } private Object getSourceObject(BonusObj bo) { Object source = activeBonusBySource.get(bo); if (source == null) { TempBonusInfo tbi = tempBonusBySource.get(bo); if (tbi != null) { source = tbi.source; } } return source; } public List<BonusPair> getStringListFromBonus(BonusObj bo) { Object creatorObj = getSourceObject(bo); List<String> associatedList; CDOMObject anObj = null; if (creatorObj instanceof CDOMObject) { anObj = (CDOMObject) creatorObj; associatedList = pc.getConsolidatedAssociationList(anObj); if (associatedList == null || associatedList.isEmpty()) { associatedList = NO_ASSOC_LIST; } } else { associatedList = NO_ASSOC_LIST; } List<BonusPair> bonusList = new ArrayList<>(); // Must use getBonusName because it contains the unaltered bonusType String bonusName = bo.getBonusName(); String[] bonusInfoArray = bo.getBonusInfo().split(","); String bonusType = bo.getTypeString(); for (String assoc : associatedList) { String replacedName; if (bonusName.indexOf(VALUE_TOKEN_REPLACEMENT) >= 0) { replacedName = bonusName.replaceAll(VALUE_TOKEN_PATTERN, assoc); } else { replacedName = bonusName; } List<String> replacedInfoList = new ArrayList<>(4); for (String bonusInfo : bonusInfoArray) { if (bonusInfo.indexOf(VALUE_TOKEN_REPLACEMENT) >= 0) { replacedInfoList.add(bonusInfo.replaceAll( VALUE_TOKEN_PATTERN, assoc)); } else if (bonusInfo.indexOf(VAR_TOKEN_REPLACEMENT) >= 0) { replacedInfoList.add(bonusName .replaceAll(VAR_TOKEN_PATTERN, assoc)); } else if (bonusInfo.equals(LIST_TOKEN_REPLACEMENT)) { replacedInfoList.add(assoc); } else { replacedInfoList.add(bonusInfo); } } Formula newFormula; if (bo.isValueStatic()) { newFormula = bo.getFormula(); } else { String value = bo.getValue(); // A %LIST substitution also needs to be done in the val // section int listIndex = value.indexOf(VALUE_TOKEN_REPLACEMENT); String thisValue = value; if (listIndex >= 0) { thisValue = value.replaceAll(VALUE_TOKEN_PATTERN, assoc); } //Need to protect against a selection not being made with a %LIST if (thisValue.isEmpty()) { thisValue = "0"; } newFormula = FormulaFactory.getFormulaFor(thisValue); } for (String replacedInfo : replacedInfoList) { StringBuilder sb = new StringBuilder(100); sb.append(replacedName).append('.').append(replacedInfo); if (bo.hasTypeString()) { sb.append(':').append(bonusType); } bonusList.add(new BonusPair(sb.toString(), newFormula, creatorObj)); } } return bonusList; } public static class TempBonusInfo { public final Object source; public final Object target; public TempBonusInfo(Object src, Object tgt) { source = src; target = tgt; } } public double calcBonusesWithCost(List<BonusObj> list) { double totalBonus = 0; for (BonusObj aBonus : list) { final CDOMObject anObj = (CDOMObject) getSourceObject(aBonus); if (anObj == null) { continue; } double iBonus = 0; if (aBonus.qualifies(pc, anObj)) { iBonus = aBonus.resolve(pc, anObj.getQualifiedKey()) .doubleValue(); } int k; if (ChooseActivation.hasNewChooseToken(anObj)) { k = 0; for (String aString : pc.getConsolidatedAssociationList(anObj)) { if (aString.equalsIgnoreCase(aBonus.getBonusInfo())) { ++k; } } } else { k = 1; } if ((k == 0) && !CoreUtility.doublesEqual(iBonus, 0)) { totalBonus += iBonus; } else { totalBonus += (iBonus * k); } } return totalBonus; } public boolean hasTempBonusesApplied(CDOMObject mod) { for (TempBonusInfo tbi : tempBonusBySource.values()) { if (tbi.source.equals(mod)) { return true; } } return false; } private Map<BonusObj, Object> getAllActiveBonuses() { Map<BonusObj, Object> ret = new IdentityHashMap<>(); for (final BonusContainer pobj : pc.getBonusContainerList()) { // We exclude equipmods here as their bonuses are already counted in // the equipment they belong to. if (pobj != null && !(pobj instanceof EquipmentModifier)) { boolean use = true; if (pobj instanceof PCClass) { // Class bonuses are only included if the level is greater // than 0 // This is because 0 levels of a class can be added to // access spell casting etc use = pc.getLevel(((PCClass) pobj)) > 0; } if (use) { pobj.activateBonuses(pc); List<BonusObj> abs = pobj.getActiveBonuses(pc); for (BonusObj bo : abs) { ret.put(bo, pobj); } } } } if (pc.getUseTempMods()) { ret.putAll(getTempBonuses()); } return ret; } /** * Report the change in bonuses from the last checkpoint to the log. */ public void logChangeFromCheckpoint() { Map<String, String> addedMap = new HashMap<>(activeBonusMap); for (Entry<String, String> prevEntry : checkpointMap.entrySet()) { String addedValue = addedMap.get(prevEntry.getKey()); if (prevEntry.getValue().equals(addedValue)) { addedMap.remove(prevEntry.getKey()); } } Map<String, String> removedMap = new HashMap<>(checkpointMap); for (Entry<String, String> prevEntry : activeBonusMap.entrySet()) { String addedValue = removedMap.get(prevEntry.getKey()); if (prevEntry.getValue().equals(addedValue)) { removedMap.remove(prevEntry.getKey()); } } Logging.errorPrint("..Bonuses removed last round: " + removedMap); Logging.errorPrint("..Bonuses added last round: " + addedMap); } }