/* * Copyright (c) 2010 Tom Parker <thpr@users.sourceforge.net> * This program 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 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 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., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package pcgen.cdom.facet.analysis; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import pcgen.base.util.NamedValue; import pcgen.cdom.base.CDOMObject; import pcgen.cdom.base.Constants; import pcgen.cdom.base.FormulaFactory; import pcgen.cdom.enumeration.CharID; import pcgen.cdom.facet.BonusCheckingFacet; import pcgen.cdom.facet.EquipmentFacet; import pcgen.cdom.facet.FormulaResolvingFacet; import pcgen.cdom.facet.base.AbstractStorageFacet; import pcgen.cdom.facet.event.DataFacetChangeEvent; import pcgen.cdom.facet.event.DataFacetChangeListener; import pcgen.cdom.facet.model.DeityFacet; import pcgen.cdom.facet.model.RaceFacet; import pcgen.cdom.facet.model.TemplateFacet; import pcgen.core.Equipment; import pcgen.core.Globals; import pcgen.core.Movement; import pcgen.core.Race; import pcgen.core.SettingsHandler; import pcgen.core.utils.CoreUtility; import pcgen.util.enumeration.Load; /** * MovementResultFacet stores the resulting movement of a Player Character. Note * that this does not store the Movement objects granted by CDOMObjects; rather * this is storing the resulting values post aggregation of those Movement * objects. * * @author Thomas Parker (thpr [at] yahoo.com) */ public class MovementResultFacet extends AbstractStorageFacet<CharID> implements DataFacetChangeListener<CharID, CDOMObject> { private MovementFacet movementFacet; private BaseMovementFacet baseMovementFacet; private RaceFacet raceFacet; private TemplateFacet templateFacet; private DeityFacet deityFacet; private EquipmentFacet equipmentFacet; private BonusCheckingFacet bonusCheckingFacet; private UnencumberedArmorFacet unencumberedArmorFacet; private UnencumberedLoadFacet unencumberedLoadFacet; private FormulaResolvingFacet formulaResolvingFacet; private LoadFacet loadFacet; private static final double[] EMPTY_DOUBLE_ARRAY = new double[0]; /** * Returns the movement value of the given type for the Player Character * identified by the given CharID. All appropriate BONUSes are added to the * movement before the result is returned. * * @param id * The CharID identifying the Player Character for which the * movement value of the given type to be returned * @param moveType * The movement type to be returned * @return The movement value of the given type for the Player Character * identified by the given CharID */ public double movementOfType(CharID id, String moveType) { MovementCacheInfo mci = getInfo(id); if (mci == null) { return 0.0; } return mci.movementOfType(id, moveType); } /** * Returns the type-safe MovementCacheInfo for this MoneyFacet and the given * CharID. Will return a new, empty MovementCacheInfo if no Money * information has been set for the given CharID. Will not return null. * * Note that this method SHOULD NOT be public. The MovementCacheInfo object * is owned by MoneyFacet, and since it can be modified, a reference to that * object should not be exposed to any object other than MoneyFacet. * * @param id * The CharID for which the MovementCacheInfo should be returned * @return The MovementCacheInfo for the Player Character represented by the * given CharID. */ private MovementCacheInfo getConstructingInfo(CharID id) { MovementCacheInfo rci = getInfo(id); if (rci == null) { rci = new MovementCacheInfo(); setCache(id, rci); } return rci; } /** * Returns the type-safe MovementCacheInfo for this MoneyFacet and the given * CharID. May return null if no Movement information has been set for the * given CharID. * * Note that this method SHOULD NOT be public. The MovementCacheInfo object * is owned by MoneyFacet, and since it can be modified, a reference to that * object should not be exposed to any object other than MoneyFacet. * * @param id * The CharID for which the MovementCacheInfo should be returned * @return The MovementCacheInfo for the Player Character represented by the * given CharID; null if no Movement information has been set for * the Player Character. */ private MovementCacheInfo getInfo(CharID id) { return (MovementCacheInfo) getCache(id); } /** * Data structure that stores the actual movement values for a Player * Character. */ public class MovementCacheInfo { private double[] movementMult = EMPTY_DOUBLE_ARRAY; private String[] movementMultOp = Globals.EMPTY_STRING_ARRAY; private String[] movementTypes = Globals.EMPTY_STRING_ARRAY; // Movement lists private double[] movements = EMPTY_DOUBLE_ARRAY; /** * Returns the movement value of the given type for the Player * Character. All appropriate BONUSes are added to the movement before * the result is returned. * * @param moveType * The movement type to be returned * @return The movement value of the given type for the Player Character */ public double movementOfType(CharID id, String moveType) { if (movementTypes == null) { return 0.0; } for (int moveIdx = 0; moveIdx < movementTypes.length; moveIdx++) { if (movementTypes[moveIdx].equalsIgnoreCase(moveType)) { return movement(id, moveIdx); } } return 0.0; } public int countMovementTypes() { return (movements != null) ? movements.length : 0; } /** * recalculate all the move rates and modifiers */ public void adjustMoveRates(CharID id) { movementMult = EMPTY_DOUBLE_ARRAY; movementMultOp = Globals.EMPTY_STRING_ARRAY; movementTypes = Globals.EMPTY_STRING_ARRAY; movements = EMPTY_DOUBLE_ARRAY; Race race = raceFacet.get(id); if (race == null) { return; } Set<Movement> mms = baseMovementFacet.getSet(id); if (mms == null || mms.isEmpty()) { return; } Movement movement = mms.iterator().next(); movements = movement.getMovements(); movementTypes = movement.getMovementTypes(); movementMult = movement.getMovementMult(); movementMultOp = movement.getMovementMultOp(); for (Movement mv : movementFacet.getSet(id)) { for (int i1 = 0; i1 < mv.getNumberOfMovements(); i1++) { if (mv.getMovementType(i1) != null) { setMyMoveRates(mv.getMovementType(i1), mv.getMovement(i1), mv.getMovementMult(i1), mv.getMovementMultOp(i1), mv.getMoveRatesFlag()); } } } // Need to create movement entries if there is a BONUS:MOVEADD // associated with that type of movement for (String moveType : bonusCheckingFacet.getExpandedBonusInfo(id, "MOVEADD")) { if (moveType.startsWith("TYPE")) { moveType = moveType.substring(5); } if (!moveType.equalsIgnoreCase("ALL")) { moveType = CoreUtility.capitalizeFirstLetter(moveType); boolean found = false; for (int i = 0; i < movements.length; i++) { if (moveType.equals(movementTypes[i])) { found = true; } } if (!found) { setMyMoveRates(moveType, 0.0, 0.0, "", 0); } } } } /** * sets up the movement arrays creates them if they do not exist * * @param moveType * @param anDouble * @param moveMult * @param multOp * @param moveFlag */ private void setMyMoveRates(String moveType, double anDouble, double moveMult, String multOp, int moveFlag) { // // NOTE: can not use getMovements() accessor as it calls // this function, so use the variable: movements // double moveRate; // The ALL type can only be applied to existing movement // so just loop and add or set as appropriate if ("ALL".equals(moveType)) { if (moveFlag == 0) { // set all types of movement to moveRate for (int i = 0; i < movements.length; i++) { moveRate = anDouble; movements[i] = moveRate; } } else { // add moveRate to all types of movement. for (int i = 0; i < movements.length; i++) { moveRate = anDouble + movements[i]; movements[i] = moveRate; } } } else { if (moveFlag == 0) { // set movement to moveRate moveRate = anDouble; for (int i = 0; i < movements.length; i++) { if (moveType.equals(movementTypes[i])) { if (moveRate > movements[i]) { movements[i] = moveRate; } if (multOp != null && (movementMultOp[i] == null || !multOp.isEmpty())) { movementMult[i] = moveMult; movementMultOp[i] = multOp; } return; } } increaseMoveArray(moveRate, moveType, moveMult, multOp); } else { // get base movement, then add moveRate moveRate = anDouble + movements[0]; // for existing types of movement: for (int i = 0; i < movements.length; i++) { if (moveType.equals(movementTypes[i])) { movements[i] = moveRate; movementMult[i] = moveMult; movementMultOp[i] = multOp; return; } } increaseMoveArray(moveRate, moveType, moveMult, multOp); } } } private void increaseMoveArray(double moveRate, String moveType, Double moveMult, String multOp) { // could not find an existing one so // need to add new item to array // double[] tempMove = movements; String[] tempType = movementTypes; double[] tempMult = movementMult; String[] tempMultOp = movementMultOp; // now increase the size of the array by one movements = new double[tempMove.length + 1]; movementTypes = new String[tempMove.length + 1]; movementMult = new double[tempMove.length + 1]; movementMultOp = new String[tempMove.length + 1]; System.arraycopy(tempMove, 0, movements, 0, tempMove.length); System.arraycopy(tempType, 0, movementTypes, 0, tempMove.length); System.arraycopy(tempMult, 0, movementMult, 0, tempMove.length); System.arraycopy(tempMultOp, 0, movementMultOp, 0, tempMove.length); // the size is larger, but arrays start at 0 // so an array length=3 would have 0, 1, 2 as the targets movements[tempMove.length] = moveRate; movementTypes[tempMove.length] = moveType; movementMult[tempMove.length] = moveMult; movementMultOp[tempMove.length] = multOp; } /** * get the base MOVE: plus any bonuses from BONUS:MOVE additions takes * into account Armor restrictions to movement and load carried * * @param moveIdx * @return movement */ public double movement(CharID id, int moveIdx) { // get base movement double moveInFeet = getMovement(moveIdx); // First get the MOVEADD bonus moveInFeet += bonusCheckingFacet.getBonus(id, "MOVEADD", "TYPE." + getMovementType(moveIdx).toUpperCase()); // also check for special case of TYPE=ALL moveInFeet += bonusCheckingFacet.getBonus(id, "MOVEADD", "TYPE.ALL"); double calcMove = moveInFeet; // now we apply any multipliers to the BASE move + MOVEADD move // First we get possible multipliers/divisors from the MOVE: // MOVEA: and MOVECLONE: tags if (getMovementMult(moveIdx) > 0) { calcMove = calcMoveMult(moveInFeet, moveIdx); } // Now we get the BONUS:MOVEMULT multipliers double moveMult = bonusCheckingFacet.getBonus(id, "MOVEMULT", "TYPE." + getMovementType(moveIdx).toUpperCase()); // also check for special case of TYPE=ALL moveMult += bonusCheckingFacet.getBonus(id, "MOVEMULT", "TYPE.ALL"); if (moveMult > 0) { calcMove = (int) (calcMove * moveMult); } double postMove = calcMove; // now add on any POSTMOVE bonuses postMove += bonusCheckingFacet.getBonus(id, "POSTMOVEADD", "TYPE." + getMovementType(moveIdx).toUpperCase()); // also check for special case of TYPE=ALL postMove += bonusCheckingFacet.getBonus(id, "POSTMOVEADD", "TYPE.ALL"); // because POSTMOVE is magical movement which should not be // multiplied by magical items, etc, we now see which is larger, // (baseMove + postMove) or (baseMove * moveMultiplier) // and keep the larger one, discarding the other moveInFeet = Math.max(calcMove, postMove); // get a list of all equipped Armor Load armorLoad = Load.LIGHT; for (Equipment eq : equipmentFacet.getSet(id)) { if (!eq.typeStringContains("Armor") || !eq.isEquipped() || eq.isShield()) { continue; } if (eq.isHeavy() && !unencumberedArmorFacet.ignoreLoad(id, Load.HEAVY)) { armorLoad = armorLoad.max(Load.HEAVY); } else if (eq.isMedium() && !unencumberedArmorFacet.ignoreLoad(id, Load.MEDIUM)) { armorLoad = armorLoad.max(Load.MEDIUM); } } double armorMove = Globals .calcEncumberedMove(armorLoad, moveInFeet); Load pcLoad = loadFacet.getLoadType(id); double loadMove = calcEncumberedMove(id, pcLoad, moveInFeet); // It is possible to have a PC that is not encumbered by Armor // But is encumbered by Weight carried (and visa-versa) // So do two calcs and take the slowest moveInFeet = Math.min(armorMove, loadMove); return moveInFeet; } /** * @param moveIdx * @return the integer movement speed for Index */ private double getMovement(int moveIdx) { if ((movements != null) && (moveIdx < movements.length)) { return movements[moveIdx]; } return 0.0d; } public String getMovementType(int moveIdx) { if ((movementTypes != null) && (moveIdx < movementTypes.length)) { return movementTypes[moveIdx]; } return Constants.EMPTY_STRING; } /** * @param moveIdx * @return the integer movement speed multiplier for Index */ private double getMovementMult(int moveIdx) { if ((movements != null) && (moveIdx < movementMult.length)) { return movementMult[moveIdx]; } return 0.0d; } private double calcMoveMult(double move, int index) { double iMove = 0; if (movementMultOp[index].charAt(0) == '*') { iMove = move * movementMult[index]; } else if (movementMultOp[index].charAt(0) == '/') { iMove = move / movementMult[index]; } if (iMove > 0) { return iMove; } return move; } public List<NamedValue> getMovementValues(CharID id) { List<NamedValue> list = new ArrayList<>(); for (int i = 0; i < countMovementTypes(); i++) { list.add(new NamedValue(getMovementType(i), movement(id, i))); } return list; } /** * Returns the base movement value of the given type for the Player * Character. No BONUSes are added to the movement before it is * returned. * * @param moveType * The movement type to be returned * @return The movement value of the given type for the Player Character */ public double getMovementOfType(String moveType) { for (int x = 0; x < countMovementTypes(); ++x) { String type = getMovementType(x); if (moveType.equalsIgnoreCase(type)) { return getMovement(x); } } return 0.0d; } /** * Returns the base movement value of the given type for the Player * Character, when the Player Character is under the given Load. No * BONUSes are added to the movement before it is returned. * * @param moveType * The movement type to be returned * @param load * The Load to be used to calculate the base movement of the * Player Character * @return The movement value of the given type for the Player Character */ public int getBaseMovement(String moveType, Load load) { for (int i = 0; i < countMovementTypes(); i++) { if (getMovementType(i).equalsIgnoreCase(moveType)) { return (int) getMovement(i); } } return 0; } /** * Returns true if the Player Character has a movement value of the * given type. * * @param moveType * The movement type to be tested to see if the Player * Character has a movement value of this type * @return true if the Player Character has a movement value of the * given type; false otherwise */ public boolean hasMovement(String moveType) { for (int i = 0; i < countMovementTypes(); i++) { if (getMovementType(i).equalsIgnoreCase(moveType)) { return true; } } return false; } /** * Works for dnd according to the method noted in the faq. (NOTE: The * table in the dnd faq is wrong for speeds 80 and 90) Not as sure it * works for all other d20 games. * * @param load * @param unencumberedMove * the unencumbered move value * @return encumbered move as an integer */ public double calcEncumberedMove(CharID id, Load load, double unencumberedMove) { double encumberedMove; // // Can we ignore any encumberance for this type? If we can, then // there's // no // need to do any more calculations. // if (unencumberedLoadFacet.ignoreLoad(id, load)) { encumberedMove = unencumberedMove; } else { String formula = SettingsHandler.getGame().getLoadInfo() .getLoadMoveFormula(load.toString()); if (!formula.isEmpty()) { formula = formula.replaceAll(Pattern.quote("$$MOVE$$"), Double.toString(Math.floor(unencumberedMove))); return formulaResolvingFacet.resolve(id, FormulaFactory.getFormulaFor(formula), "") .doubleValue(); } return Globals.calcEncumberedMove(load, unencumberedMove); } return encumberedMove; } @Override public int hashCode() { return (movementTypes.length == 0) ? -1 : movementTypes[0].hashCode(); } @Override public boolean equals(Object o) { if (o == this) { return true; } if (o instanceof MovementCacheInfo) { MovementCacheInfo ci = (MovementCacheInfo) o; return Arrays.equals(movementMult, ci.movementMult) && Arrays.deepEquals(movementMultOp, ci.movementMultOp) && Arrays.deepEquals(movementTypes, ci.movementTypes) && Arrays.equals(movements, ci.movements); } return false; } } /** * Returns the number of movement types for the Player Character identified * by the given CharID. * * @param id * The CharID identifying the Player Character for which the * number of movement types is to be returned * @return The number of movement types for the Player Character identified * by the given CharID */ public int countMovementTypes(CharID id) { MovementCacheInfo mci = getInfo(id); if (mci == null) { return 0; } return mci.countMovementTypes(); } /** * Recalculates all movement values for the Player Character identified by * the given CharID. * * @param id * The CharID for which all of the movement values is to be * recalculated */ public void reset(CharID id) { getConstructingInfo(id).adjustMoveRates(id); } /** * Returns a non-null List of the movement values for the Player Character * represented by the given CharID. * * This method is value-semantic in that ownership of the returned List is * transferred to the class calling this method. Modification of the * returned List will not modify this MovementResultFacet and modification * of this MovementResultFacet will not modify the returned List. * Modifications to the returned List will also not modify any future or * previous objects returned by this (or other) methods on * MovementResultFacet. If you wish to modify the information stored in this * MovementResultFacet, you must add Movement objects to the Player * Character and call reset(CharID). * * @param id * The CharID identifying the Player Character for which the * movement values should be returned * @return A non-null List of the movement values for the Player Character * represented by the given CharID. */ public List<NamedValue> getMovementValues(CharID id) { MovementCacheInfo mci = getInfo(id); if (mci == null) { return Collections.emptyList(); } return mci.getMovementValues(id); } /** * Returns the base movement value of the given type for the Player * Character identified by the given CharID. No BONUSes are added to the * movement before it is returned. * * @param id * The CharID identifying the Player Character for which the * movement value of the given type to be returned * @param moveType * The movement type to be returned * @return The movement value of the given type for the Player Character * identified by the given CharID */ public double getMovementOfType(CharID id, String moveType) { MovementCacheInfo mci = getInfo(id); if (mci == null) { return 0.0d; } return mci.getMovementOfType(moveType); } /** * Returns the base movement value of the given type for the Player * Character identified by the given CharID, when the Player Character is * under the given Load. No BONUSes are added to the movement before it is * returned. * * @param id * The CharID identifying the Player Character for which the * movement value of the given type to be returned * @param moveType * The movement type to be returned * @param load * The Load to be used to calculate the base movement of the * Player Character * @return The movement value of the given type for the Player Character * identified by the given CharID */ public int getBaseMovement(CharID id, String moveType, Load load) { MovementCacheInfo mci = getInfo(id); if (mci == null) { return 0; } return mci.getBaseMovement(moveType, load); } /** * Returns true if the Player Character identified by the given CharID has a * movement value of the given type. * * @param id * The CharID identifying the Player Character which will be * tested to see if it contains a movement of the given type * @param moveType * The movement type to be tested to see if the Player Character * has a movement value of this type * @return true if the Player Character identified by the given CharID has a * movement value of the given type; false otherwise */ public boolean hasMovement(CharID id, String moveType) { MovementCacheInfo mci = getInfo(id); if (mci == null) { return false; } return mci.hasMovement(moveType); } /** * Triggers a full recalculation of Player Character movement when a * CDOMObject is added to a Player Character. * * Triggered when one of the Facets to which MovementResultFacet listens * fires a DataFacetChangeEvent to indicate a CDOMObject was added to a * Player Character. * * @param dfce * The DataFacetChangeEvent containing the information about the * change * * @see pcgen.cdom.facet.event.DataFacetChangeListener#dataAdded(pcgen.cdom.facet.event.DataFacetChangeEvent) */ @Override public void dataAdded(DataFacetChangeEvent<CharID, CDOMObject> dfce) { reset(dfce.getCharID()); } /** * Triggers a full recalculation of Player Character movement when a * CDOMObject is added to a Player Character. * * Triggered when one of the Facets to which MovementResultFacet listens * fires a DataFacetChangeEvent to indicate a CDOMObject was removed from a * Player Character. * * @param dfce * The DataFacetChangeEvent containing the information about the * change * * @see pcgen.cdom.facet.event.DataFacetChangeListener#dataRemoved(pcgen.cdom.facet.event.DataFacetChangeEvent) */ @Override public void dataRemoved(DataFacetChangeEvent<CharID, CDOMObject> dfce) { reset(dfce.getCharID()); } public void setMovementFacet(MovementFacet movementFacet) { this.movementFacet = movementFacet; } public void setBaseMovementFacet(BaseMovementFacet baseMovementFacet) { this.baseMovementFacet = baseMovementFacet; } public void setRaceFacet(RaceFacet raceFacet) { this.raceFacet = raceFacet; } public void setTemplateFacet(TemplateFacet templateFacet) { this.templateFacet = templateFacet; } public void setDeityFacet(DeityFacet deityFacet) { this.deityFacet = deityFacet; } public void setEquipmentFacet(EquipmentFacet equipmentFacet) { this.equipmentFacet = equipmentFacet; } public void setBonusCheckingFacet(BonusCheckingFacet bonusCheckingFacet) { this.bonusCheckingFacet = bonusCheckingFacet; } public void setUnencumberedArmorFacet( UnencumberedArmorFacet unencumberedArmorFacet) { this.unencumberedArmorFacet = unencumberedArmorFacet; } public void setUnencumberedLoadFacet(UnencumberedLoadFacet unencumberedLoadFacet) { this.unencumberedLoadFacet = unencumberedLoadFacet; } public void setFormulaResolvingFacet(FormulaResolvingFacet formulaResolvingFacet) { this.formulaResolvingFacet = formulaResolvingFacet; } public void setLoadFacet(LoadFacet loadFacet) { this.loadFacet = loadFacet; } /** * Initializes the connections for MovementResultFacet to other facets. * * This method is automatically called by the Spring framework during * initialization of the MovementResultFacet. */ public void init() { raceFacet.addDataFacetChangeListener(2000, this); deityFacet.addDataFacetChangeListener(2000, this); templateFacet.addDataFacetChangeListener(2000, this); } /** * Copies the contents of the MovementResultFacet from one Player Character * to another Player Character, based on the given CharIDs representing * those Player Characters. * * This is a method in MovementResultFacet in order to avoid exposing the * mutable Map object to other classes. This should not be inlined, as the * Map is internal information to MovementResultFacet and should not be * exposed to other classes. * * Note also the copy is a one-time event and no references are maintained * between the Player Characters represented by the given CharIDs (meaning * once this copy takes place, any change to the MovementResultFacet of one * Player Character will only impact the Player Character where the * MovementResultFacet was changed). * * @param source * The CharID representing the Player Character from which the * information should be copied * @param copy * The CharID representing the Player Character to which the * information should be copied */ @Override public void copyContents(CharID source, CharID copy) { MovementCacheInfo mci = getInfo(source); if (mci != null) { MovementCacheInfo copymci = getConstructingInfo(copy); if (mci.movementMult != null) { copymci.movementMult = mci.movementMult.clone(); } if (mci.movementMultOp != null) { copymci.movementMultOp = mci.movementMultOp.clone(); } if (mci.movements != null) { copymci.movements = mci.movements.clone(); } if (mci.movementTypes != null) { copymci.movementTypes = mci.movementTypes.clone(); } } } }