Java tutorial
/* * Copyright 2001 (C) Bryan McRoberts <merton.monk@codemonkeypublishing.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.io.BufferedWriter; import java.io.File; import java.io.Serializable; import java.math.BigDecimal; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.SortedSet; import java.util.StringTokenizer; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.commons.lang3.StringUtils; import pcgen.base.formula.Formula; import pcgen.base.lang.StringUtil; import pcgen.base.util.FixedStringList; import pcgen.cdom.base.CDOMObject; import pcgen.cdom.base.Constants; import pcgen.cdom.base.PrereqObject; import pcgen.cdom.enumeration.AssociationListKey; import pcgen.cdom.enumeration.CharID; import pcgen.cdom.enumeration.EqModFormatCat; import pcgen.cdom.enumeration.EquipmentLocation; import pcgen.cdom.enumeration.FormulaKey; import pcgen.cdom.enumeration.IntegerKey; import pcgen.cdom.enumeration.ListKey; import pcgen.cdom.enumeration.ObjectKey; import pcgen.cdom.enumeration.StringKey; import pcgen.cdom.enumeration.Type; import pcgen.cdom.facet.FacetLibrary; import pcgen.cdom.facet.analysis.ResultFacet; import pcgen.cdom.formula.PCGenScoped; import pcgen.cdom.helper.Capacity; import pcgen.cdom.inst.EqSizePenalty; import pcgen.cdom.inst.EquipmentHead; import pcgen.cdom.processor.ChangeArmorType; import pcgen.cdom.reference.CDOMDirectSingleRef; import pcgen.cdom.reference.CDOMSingleRef; import pcgen.cdom.util.CControl; import pcgen.core.analysis.BonusActivation; import pcgen.core.analysis.BonusCalc; import pcgen.core.analysis.EqModCost; import pcgen.core.analysis.EqModSpellInfo; import pcgen.core.analysis.EquipmentChoiceDriver; import pcgen.core.analysis.SizeUtilities; import pcgen.core.bonus.Bonus; import pcgen.core.bonus.BonusObj; import pcgen.core.bonus.BonusUtilities; import pcgen.core.character.EquipSlot; import pcgen.core.character.WieldCategory; import pcgen.core.prereq.PrereqHandler; import pcgen.core.prereq.Prerequisite; import pcgen.core.utils.CoreUtility; import pcgen.core.utils.MessageType; import pcgen.core.utils.ShowMessageDelegate; import pcgen.facade.core.EquipmentFacade; import pcgen.io.FileAccess; import pcgen.io.exporttoken.EqToken; import pcgen.rules.context.AbstractReferenceContext; import pcgen.util.BigDecimalHelper; import pcgen.util.JEPResourceChecker; import pcgen.util.Logging; import pcgen.util.PJEP; import pcgen.util.PjepPool; import pcgen.util.enumeration.Load; import pcgen.util.enumeration.View; import pcgen.util.enumeration.Visibility; /** * Represents Equipment for a PC. */ public final class Equipment extends PObject implements Serializable, Comparable<Object>, VariableContainer, EquipmentFacade, PCGenScoped { private static final long serialVersionUID = 1; private static final String EQMOD_WEIGHT = "_WEIGHTADD"; private static final String EQMOD_DAMAGE = "_DAMAGE"; private static final SortedSet<String> S_EQUIPMENT_TYPES = new TreeSet<>(); private AssociationSupport assocSupt = new AssociationSupport(); private BigDecimal costMod = BigDecimal.ZERO; private Equipment d_parent; private List<Equipment> d_containedEquipment = new ArrayList<>(); private Float carried = (float) 0; // OwnedItem private EquipmentLocation location = EquipmentLocation.NOT_CARRIED; // OwnedItem private boolean equipped; // OwnedItem private int numberEquipped; private Map<String, Float> d_childTypes = new HashMap<>(); private String containerCapacityString = null; private String containerContentsString = ""; private String appliedBonusName = ""; private String indexedUnderType = ""; private String wholeItemName = ""; private String modifiedName = ""; private String moveString = ""; // player added note private String noteString = ""; private boolean automatic; private boolean bonusPrimary = true; private boolean calculatingCost; private boolean weightAlreadyUsed; private double qty; private int outputIndex; private int outputSubindex; private List<String> typeListCachePrimary; private List<String> typeListCacheSecondary; private boolean usePrimaryCache; private boolean useSecondaryCache; private boolean dirty; private String cachedNameWithoutCharges; private String cachedNameWithCharges; /** Map of the bonuses for the object */ private Map<String, String> bonusMap; private boolean virtualItem; public Equipment() { final SizeAdjustment sizeAdj = SizeUtilities.getDefaultSizeAdjustment(); if (sizeAdj != null) { put(ObjectKey.SIZE, CDOMDirectSingleRef.getRef(sizeAdj)); } } // // Name functions // /** * Set's the Temporary Bonuses name used for Display on Output Sheets * * @param aString * Name to use for temp bonus */ public void setAppliedName(final String aString) { appliedBonusName = aString; } /** * Get Applied Name * * @return Applied name */ public String getAppliedName() { if (!appliedBonusName.isEmpty()) { final StringBuilder aString = new StringBuilder(100); aString.append(" [").append(appliedBonusName).append("]"); return aString.toString(); } return ""; } // // TYPE queries // /** * Gets the ammunition attribute of the Equipment object * * @return The ammunition value */ public boolean isAmmunition() { return isType("AMMUNITION"); } /** * Gets the armor attribute of the Equipment object * * @return The armor value */ public boolean isArmor() { return isType("ARMOR"); } /** * Gets the double attribute of the Equipment object * * @return The double value */ public boolean isDouble() { return isType("DOUBLE"); } /** * Gets the eitherType attribute of the Equipment object * * @param aType * Description of the Parameter * @return The eitherType value */ public boolean isEitherType(final String aType) { return isType(aType, true) || isType(aType, false); } /** * Gets the extra attribute of the Equipment object * * @return The extra value */ public boolean isExtra() { return isType("EXTRA"); } /** * Gets the heavy attribute of the Equipment object * * @return The heavy value */ public boolean isHeavy() { return isType("HEAVY"); } /** * Gets the medium attribute of the Equipment object * * @return The medium value */ public boolean isMedium() { return isType("MEDIUM"); } /** * Gets the light attribute of the Equipment object * * @return The light value */ public boolean isLight() { return isType("LIGHT"); } /** * Gets the magic attribute of the Equipment object * * @return The magic value */ public boolean isMagic() { return isType("MAGIC"); } /** * Gets the melee attribute of the Equipment object * * @return The melee value */ public boolean isMelee() { return isType("MELEE"); } /** * Gets the monk attribute of the Equipment object * * @return The monk value */ public boolean isMonk() { return isType("MONK"); } /** * Gets the natural weapon attribute of the Equipment object * * @return The natural value */ public boolean isNatural() { return isType("NATURAL"); } /** * Identify if this is a primary natural weapon. * @return true for a primary natural weapons, false if not natural or a secondary natural weapon. */ public boolean isPrimaryNaturalWeapon() { // The name is generated by the NATURALATTACKS token, so we place some trust in it. return isNatural() && modifiedName().endsWith("Primary"); //$NON-NLS-1$ } /** * Gets the ranged attribute of the Equipment object * * @return The ranged value */ public boolean isRanged() { return isType("RANGED"); } /** * Gets the shield attribute of the Equipment object * * @return The shield value */ public boolean isShield() { return isType("SHIELD"); } /** * Gets the suit attribute of the Equipment object * * @return The suit value */ private boolean isSuit() { return isType("SUIT"); } /** * Gets the thrown attribute of the Equipment object * * @return The thrown value */ public boolean isThrown() { return isType("THROWN"); } /** * Gets the type attribute of the Equipment object * * @return The type */ @Override public String getType() { return getType(true); } /** * Gets the type attribute of the Equipment object * * @param aType * Description of the Parameter * @return The type value */ @Override public boolean isType(final String aType) { return isType(aType, true); } /** * Gets the type attribute of the Equipment object * * @param aType * Description of the Parameter * @param bPrimary * Description of the Parameter * @return The type value */ public boolean isType(final String aType, final boolean bPrimary) { if (!bPrimary && !isDouble()) { return false; } final List<String> tList = typeList(bPrimary); final String myType; if (aType.startsWith("TYPE=") || aType.startsWith("TYPE.")) //$NON-NLS-1$ //$NON-NLS-2$ { myType = aType.substring(5).toUpperCase(); } else { myType = aType.toUpperCase(); } // // Must match all listed types in order to qualify // StringTokenizer tok = new StringTokenizer(myType, "."); if (tok.hasMoreTokens()) { while (tok.hasMoreTokens()) { final String type = tok.nextToken(); //CONSIDER Faster method? Case sensitivity is a problem for containsInList boolean found = false; if (tList != null) { found = tList.stream().anyMatch(type::equalsIgnoreCase); } if (!found) { return false; } } return true; } return tList.contains(aType); } /** * Gets the unarmed attribute of the Equipment object * * @return The unarmed value */ public boolean isUnarmed() { return isType("UNARMED"); } /** * Gets the weapon attribute of the Equipment object * * @return The weapon value */ public boolean isWeapon() { return isType("WEAPON"); } /** * Gets the masterwork attribute of the Equipment object * * @return The masterwork value */ boolean isMasterwork() { return isType("MASTERWORK"); } /** * Identifies if this item is one that is sold like cash (e.g. coins, trade goods) * @return true if the item is tradeable */ public boolean isSellAsCash() { return isType("Coin") || isType("Gem") //$NON-NLS-1$ //$NON-NLS-2$ || isType("Trade"); //$NON-NLS-1$ } /** * Description of the Method * * @param aString * Description of the Parameter * @return Description of the Return Value */ public boolean typeStringContains(final String aString) { return isType(aString); } /** * Gets the projectile attribute of the Equipment object * * @return The projectile value */ public boolean isProjectile() { // return isType("PROJECTILE"); return isRanged() && !isThrown(); } /** * returns all BonusObj's that are "active" * * @param aPC * PlayerCharacter used to check prereqs for bonuses * @return active bonuses */ @Override public List<BonusObj> getActiveBonuses(final PlayerCharacter aPC) { final List<BonusObj> aList = getRawBonusList(aPC).stream().filter(aPC::isApplied) .collect(Collectors.toList()); List<EquipmentModifier> eqModList = getEqModifierList(true); eqModList.stream().map(eqMod -> eqMod.getActiveBonuses(this, aPC)).forEach(aList::addAll); eqModList = getEqModifierList(false); eqModList.stream().map(eqMod -> eqMod.getActiveBonuses(this, aPC)).forEach(aList::addAll); return aList; } /** * get a list of BonusObj's of aType and aName * @param pc * The PC with the Equipment * @param aType * a TYPE of bonus (such as "COMBAT" or "SKILL") * @param aName * the NAME of the bonus (such as "ATTACKS" or "SPOT") * @param bPrimary * used for double weapons (head1 vs head2) * * @return a list of bonusObj's of aType and aName */ List<BonusObj> getBonusListOfType(PlayerCharacter pc, final String aType, final String aName, final boolean bPrimary) { final List<BonusObj> aList = new ArrayList<>(); aList.addAll(BonusUtilities.getBonusFromList(getBonusList(pc), aType, aName)); getEqModifierList(bPrimary).stream() .map(eqMod -> BonusUtilities.getBonusFromList(eqMod.getBonusList(this), aType, aName)) .forEach(aList::addAll); return aList; } // // Misc properties // public boolean isAutomatic() { return automatic; } /** * Set Automatic * * @param arg sets the isAutomatic property of the Equipment */ public void setAutomatic(final boolean arg) { automatic = arg; } /** * Gets the baseItemName attribute of the Equipment object * * @return The baseItemName value */ public String getBaseItemName() { CDOMSingleRef<Equipment> baseItem = get(ObjectKey.BASE_ITEM); if (baseItem == null) { return getKeyName(); } return baseItem.get().getDisplayName(); } /** * Gets the keyName attribute of the base item of this Equipment object. * * @return The base item's keyName value */ public String getBaseItemKeyName() { CDOMSingleRef<Equipment> baseItem = get(ObjectKey.BASE_ITEM); if (baseItem == null) { return getKeyName(); } return baseItem.get().getKeyName(); } /** * Gets the cost attribute of the Equipment object * * @param aPC The PC with the Equipment * * @return The cost value */ public BigDecimal getCost(final PlayerCharacter aPC) { BigDecimal c = BigDecimal.ZERO; if (this.isVirtual()) { return c; } // // Do pre-sizing cost increment. // eg. in the case of adamantine armor, want to add // the cost of the metal before the armor gets resized. // c = c.add(getPreSizingCostForHead(aPC, true)); c = c.add(getPreSizingCostForHead(aPC, false)); // c has cost of the item's modifications at the item's original size BigDecimal currentcost = get(ObjectKey.CURRENT_COST); if (currentcost == null) { currentcost = getSafe(ObjectKey.COST); } BigDecimal itemCost = currentcost.add(c); final List<BigDecimal> modifierCosts = new ArrayList<>(); calculatingCost = true; weightAlreadyUsed = false; EquipmentHeadCostSummary costSum = getPostSizingCostForHead(aPC, modifierCosts, true); BigDecimal nonDoubleCost = costSum.nonDoubleCost; BigDecimal c1 = costSum.postSizeCost; int iPlus = costSum.headPlus; // // Get costs from lowest to highest // if (modifierCosts.size() > 1) { Collections.sort(modifierCosts); } // Note: When calculating the second head's costs we expect not to see // any modifier costs and discard them if they do occur. These should be // applicable for weapons, which are the only dual headed items currently. EquipmentHeadCostSummary altCostSum = getPostSizingCostForHead(aPC, new ArrayList<>(), false); nonDoubleCost = nonDoubleCost.add(altCostSum.nonDoubleCost); c1 = c1.add(altCostSum.postSizeCost); int altPlus = altCostSum.headPlus; calculatingCost = false; c1 = c1.add(getCostFromPluses(iPlus, altPlus)); // Items with values less than 1 gp have their prices rounded up to 1 gp // per item // eg. 20 Arrows cost 1 gp, or 5 cp each. 1 MW Arrow costs 7 gp. // // Masterwork and Magical ammo is made in batches of 50, so the MW cost // per item should be 6 gp. This would give a cost of 6.05 gp per arrow, // 6.1 gp per bolt and 6.01 gp per bullet. // // if (c.compareTo(BigDecimal.ZERO) != 0) // { // // // // Convert to double and use math.ceil as ROUND_CEILING doesn't appear to work // // on BigDecimal.divide // final int baseQ = getBaseQty(); // itemCost = new BigDecimal(Math.ceil(itemCost.doubleValue() / baseQ) * // baseQ); // } if (!isAmmunition() && !isArmor() && !isShield() && !isWeapon()) { // // If item doesn't occupy a fixed location, then double the cost of // the modifications // DMG p.243 // if (!isMagicLimitedType()) { // // TODO: Multiple similar abilities. 100% of costliest, 75% of // next, and 50% of rest // if (!ignoresCostDouble()) { c1 = c1.subtract(nonDoubleCost).multiply(new BigDecimal("2")); c1 = c1.add(nonDoubleCost); // c = c.multiply(new BigDecimal("2")); } } else { // // Add in the cost of 2nd, 3rd, etc. modifiers again (gives // times 2) // for (int i = modifierCosts.size() - 2; i >= 0; --i) { c1 = c1.add(modifierCosts.get(i)); } } } // Don't allow the cost modifier to push the value further into the negatives if (c1.compareTo(BigDecimal.ZERO) >= 0 && c1.add(itemCost).add(costMod).compareTo(BigDecimal.ZERO) < 0) { return BigDecimal.ZERO; } return c1.add(itemCost).add(costMod); } /** * Calculate the parts of the cost for the equipment's head that are * affected by size. * * @param aPC The character who owns the equipment. * @param primaryHead Are we calculating for the primary or alternate head. * @return The cost of the head */ private BigDecimal getPreSizingCostForHead(final PlayerCharacter aPC, boolean primaryHead) { BigDecimal c = BigDecimal.ZERO; EquipmentHead head = getEquipmentHeadReference(primaryHead ? 1 : 2); if (head != null) { bonusPrimary = primaryHead; for (EquipmentModifier eqMod : head.getSafeListFor(ListKey.EQMOD)) { int iCount = getSelectCorrectedAssociationCount(eqMod); if (iCount < 1) { iCount = 1; } Formula baseCost = eqMod.getSafe(FormulaKey.BASECOST); Number bc = baseCost.resolve(this, primaryHead, aPC, ""); final BigDecimal eqModCost = new BigDecimal(bc.toString()); c = c.add(eqModCost .multiply(new BigDecimal(Integer.toString(getSafe(IntegerKey.BASE_QUANTITY) * iCount)))); c = c.add(EqModCost.addItemCosts(eqMod, aPC, "ITEMCOST", getSafe(IntegerKey.BASE_QUANTITY) * iCount, this)); } } return c; } /** * Calculate the parts of the cost for the equipment's head that are not * affected by size. * * @param aPC The character who owns the equipment. * @param modifierCosts The array of costs to be doubled if the location demands it * @param primaryHead Are we calculating for the primary or alternate head. * @return The cost, non doubling cost and total plus of the head */ private EquipmentHeadCostSummary getPostSizingCostForHead(final PlayerCharacter aPC, final List<BigDecimal> modifierCosts, boolean primaryHead) { EquipmentHeadCostSummary costSum = new EquipmentHeadCostSummary(); EquipmentHead head = getEquipmentHeadReference(primaryHead ? 1 : 2); if (head != null) { for (EquipmentModifier eqMod : head.getSafeListFor(ListKey.EQMOD)) { int iCount = getSelectCorrectedAssociationCount(eqMod); if (iCount < 1) { iCount = 1; } BigDecimal eqModCost; Formula cost = eqMod.getSafe(FormulaKey.COST); String costFormula = cost.toString(); if (hasAssociations(eqMod) && !costFormula.equals(EqModCost.getCost(eqMod, getFirstAssociation(eqMod)))) { eqModCost = BigDecimal.ZERO; for (String assoc : getAssociationList(eqMod)) { String v = calcEqModCost(aPC, EqModCost.getCost(eqMod, assoc), primaryHead); final BigDecimal thisModCost = new BigDecimal(v); eqModCost = eqModCost.add(thisModCost); if (!EqModCost.getCostDouble(eqMod)) { costSum.nonDoubleCost = costSum.nonDoubleCost.add(thisModCost); } else { modifierCosts.add(thisModCost); } } iCount = 1; } else { String v = calcEqModCost(aPC, cost.toString(), primaryHead); eqModCost = new BigDecimal(v); if (EqModCost.getCostDouble(eqMod)) { modifierCosts.add(eqModCost); } else { costSum.nonDoubleCost = costSum.nonDoubleCost.add(eqModCost); } } // Per D20 FAQ adjustments for special materials are per piece; if (eqMod.isType("BaseMaterial")) { eqModCost = eqModCost.multiply(new BigDecimal(getSafe(IntegerKey.BASE_QUANTITY))); } costSum.postSizeCost = costSum.postSizeCost.add(eqModCost); costSum.headPlus += (eqMod.getSafe(IntegerKey.PLUS) * iCount); } } return costSum; } /** * Calculates the value of a formula. Does some preprocesing for variables * that cannot be properly evaluated with just the equipment context that * is held by the variable processor. * @param aPC The character we are calculating the cost for. * @param costFormula The formula to be evaluated. * @param primaryHead Is the formula for an eqmod on the main (or only) head * @return The value of the formula */ private String calcEqModCost(final PlayerCharacter aPC, String costFormula, boolean primaryHead) { Pattern pat = Pattern.compile("BASECOST"); Matcher mat = pat.matcher(costFormula); // make string (BASECOST/X) which will be substituted into // the cost string which is then converted to a number StringBuilder sB = new StringBuilder("(BASECOST/"); sB.append(getSafe(IntegerKey.BASE_QUANTITY)); sB.append(")"); String s = mat.replaceAll(sB.toString()); String v = getVariableValue(s, "", primaryHead, aPC).toString(); return v; } /** * Set cost mod * * @param aString the cost modifier in String form */ public void setCostMod(final String aString) { try { costMod = new BigDecimal(aString); } catch (NumberFormatException e) { costMod = BigDecimal.ZERO; } } /** * Set cost mod * * @param aCost the cost modifier in BigDecimal form */ public void setCostMod(final BigDecimal aCost) { costMod = aCost; } // --------------------------- // Equipment Modifier Support // --------------------------- /** * Gets the eqModifierKeyed attribute of the Equipment object * * @param eqModKey * Description of the Parameter * @param bPrimary * if True then deal with the primary head * @return The eqModifierKeyed value */ public EquipmentModifier getEqModifierKeyed(final Object eqModKey, final boolean bPrimary) { final List<EquipmentModifier> eqModList = getEqModifierList(bPrimary); return eqModList.stream().filter(eqMod -> eqMod.getKeyName().equals(eqModKey)).findFirst().orElse(null); } /** * Gets the eqModifierList attribute of the Equipment object * * @param bPrimary * if true, get the equipment modifiers for the primary head of * the weapon * * @return The eqModifierList value */ public List<EquipmentModifier> getEqModifierList(final boolean bPrimary) { return getEquipmentHead(bPrimary ? 1 : 2).getSafeListFor(ListKey.EQMOD); } /** * Add an EquipmentModifier object to the list * * @param eqMod * The equipment modifier to add to list * @param bPrimary * if True then deal with the primary head */ public void addToEqModifierList(final EquipmentModifier eqMod, final boolean bPrimary) { if (bPrimary) { usePrimaryCache = false; } else { useSecondaryCache = false; } eqMod.setVariableParent(this); getEquipmentHead(bPrimary ? 1 : 2).addToListFor(ListKey.EQMOD, eqMod); setDirty(true); } /** * Get display information for all "interesting" properties. * * @param aPC The PC with the Equipment * * @return display string of bonuses and special properties */ public String getInterestingDisplayString(final PlayerCharacter aPC) { final StringBuilder s = new StringBuilder(100); String t = getSpecialProperties(aPC); if (t == null) { t = ""; } getActiveBonuses(aPC).stream().map(BonusObj::toString) .filter(eqBonus -> (!eqBonus.isEmpty()) && !eqBonus.startsWith("EQM")).forEach(eqBonus -> { if (s.length() != 0) { s.append(", "); } s.append(eqBonus); }); // for (final Iterator<EquipmentModifier> e = eqModifierList.iterator(); // e.hasNext();) // { // final EquipmentModifier eqMod = e.next(); // for (final Iterator<BonusObj> mI = eqMod.getBonusList().iterator(); // mI.hasNext();) // { // final BonusObj aBonus = mI.next(); // final String eqModBonus = aBonus.toString(); // if ((eqModBonus.length() != 0) && !eqModBonus.startsWith("EQM")) // { // if (s.length() != 0) // { // s.append(", "); // } // s.append(eqModBonus); // } // } // } if (!t.isEmpty()) { if (s.length() != 0) { s.append('|'); } s.append(t); } return s.toString(); } /** * Sets the isEquipped attribute of the Equipment object. * * @param aFlag * The new isEquipped value * @param aPC * The PC with the Equipment */ public void setIsEquipped(final boolean aFlag, final PlayerCharacter aPC) { equipped = aFlag; if (equipped) { activateBonuses(aPC); } else { BonusActivation.deactivateBonuses(this, aPC); } } /** * Get the item name based off the modifiers * * @return item name based off the modifiers */ public String getItemNameFromModifiers() { return getItemNameFromModifiers(getBaseItemName()); } /** * Get the item name based off the modifiers * * @param baseName base name of the object, may instead be the base key if generating a key * @return item name based off the modifiers */ private String getItemNameFromModifiers(String baseName) { CDOMSingleRef<Equipment> baseItem = get(ObjectKey.BASE_ITEM); if (baseItem == null) { return getName(); } final List<EquipmentModifier> modList; EquipmentHead head = getEquipmentHeadReference(1); if (head == null) { modList = Collections.emptyList(); } else { modList = head.getSafeListFor(ListKey.EQMOD); } EquipmentHead althead = getEquipmentHeadReference(2); final List<EquipmentModifier> altModList; if (althead == null) { altModList = Collections.emptyList(); } else { altModList = althead.getSafeListFor(ListKey.EQMOD); } final List<EquipmentModifier> commonList = new ArrayList<>(); final List<List<EquipmentModifier>> modListByFC = initSplitModList(); final List<List<EquipmentModifier>> altModListByFC = initSplitModList(); final List<List<EquipmentModifier>> commonListByFC = initSplitModList(); final Equipment baseEquipment = baseItem.get(); // Remove any modifiers on the base item so they don't confuse the // naming if (baseEquipment != null) { modList.removeAll(baseEquipment.getEqModifierList(true)); altModList.removeAll(baseEquipment.getEqModifierList(false)); } for (Iterator<EquipmentModifier> it = modList.iterator(); it.hasNext();) { EquipmentModifier eqMod = it.next(); if (eqMod.getSafe(ObjectKey.VISIBILITY).equals(Visibility.HIDDEN)) { it.remove(); } } extractListFromCommon(commonList, modList); removeCommonFromList(altModList, commonList, "eqMod expected but not found: "); // Remove masterwork from the list if magic is present suppressMasterwork(commonList); // Split the eqmod lists by format category splitModListByFormatCat(commonList, commonListByFC); splitModListByFormatCat(modList, modListByFC); splitModListByFormatCat(altModList, altModListByFC); final StringBuilder itemName = new StringBuilder(100); // Add in front eq mods int fcf = EqModFormatCat.FRONT.ordinal(); itemName.append(buildEqModDesc(commonListByFC.get(fcf), modListByFC.get(fcf), altModListByFC.get(fcf))); if (itemName.length() > 0) { itemName.append(' '); } // Add in the base name, less any modifiers baseName = baseName.trim(); int idx = baseName.indexOf('('); if (idx >= 0) { itemName.append(baseName.substring(0, idx - 1).trim()); } else { itemName.append(baseName); } // Add in middle mods int fcm = EqModFormatCat.MIDDLE.ordinal(); String eqmodDesc1 = buildEqModDesc(commonListByFC.get(fcm), modListByFC.get(fcm), altModListByFC.get(fcm)); if (!eqmodDesc1.isEmpty()) { itemName.append(' ').append(eqmodDesc1); } // Tack on the original modifiers if (idx >= 0) { itemName.append(' '); itemName.append(baseName.substring(idx)); } // Strip off the ending ')' in anticipation of more modifiers final int idx1 = itemName.toString().lastIndexOf(')'); if (idx1 >= 0) { itemName.setLength(idx1); itemName.append('/'); } else { itemName.append(" ("); } // // Put size in name if not the same as the base item // SizeAdjustment thisSize = getSafe(ObjectKey.SIZE).get(); if (!getSafe(ObjectKey.BASESIZE).get().equals(thisSize)) { itemName.append(thisSize.getDisplayName()); itemName.append('/'); } // Put in parens mods int fcp = EqModFormatCat.PARENS.ordinal(); itemName.append(buildEqModDesc(commonListByFC.get(fcp), modListByFC.get(fcp), altModListByFC.get(fcp))); // // If there were no modifiers, then drop the trailing '/' // if (itemName.toString().endsWith("/") || itemName.toString().endsWith(";")) { itemName.setLength(itemName.length() - 1); } itemName.append(')'); // If there were no modifiers, then strip the empty parenthesis final int idx2 = itemName.toString().indexOf(" ()"); if (idx2 >= 0) { itemName.setLength(idx2); } return itemName.toString(); } /** * Where a magic eqmod is present, remove the masterwork eqmod from the * list. * * @param commonList * The list of eqmods on both heads (or only head) */ private void suppressMasterwork(Collection<EquipmentModifier> commonList) { // Look for a modifier named "masterwork" (assumption: this is marked as // "assigntoall") EquipmentModifier eqMaster = commonList.stream().filter( eqMod -> "MASTERWORK".equalsIgnoreCase(eqMod.getDisplayName()) || eqMod.isIType("Masterwork")) .findFirst().orElse(null); if (eqMaster == null) { return; } if (heads.stream().anyMatch(head -> getMagicBonus(head.getListFor(ListKey.EQMOD)) != null)) { commonList.remove(eqMaster); } } /** * Build up the description of the listed equipmods for this equipment item. * Takes into account if the item is a double weapon or not. * * @param commonList * The list of common equipment modifiers. * @param modList * The list of eqmods on the primary head. * @param altModList * The list of eqmods on the secondary head. * @return The description of these equipment modifiers. */ private String buildEqModDesc(List<EquipmentModifier> commonList, List<EquipmentModifier> modList, List<EquipmentModifier> altModList) { StringBuilder desc = new StringBuilder(250); String commonDesc = getNameFromModifiers(commonList); String modDesc = getNameFromModifiers(modList); String altModDesc = getNameFromModifiers(altModList); if ((modList.isEmpty()) && (altModList.isEmpty())) { desc.append(commonDesc); } else if (!isDouble()) { desc.append(modDesc); if (!modList.isEmpty() && !commonList.isEmpty()) { desc.append('/'); } desc.append(commonDesc); } else { if (!commonDesc.isEmpty()) { desc.append(commonDesc).append(';'); } if (!modDesc.isEmpty()) { desc.append(modDesc); } else { desc.append('-'); } desc.append(';'); if (!altModDesc.isEmpty()) { desc.append(altModDesc); } else { desc.append('-'); } } return desc.toString(); } /** * OwnedItem Sets the location attribute of the Equipment object * @param newLocation * EquipmentLocation containing the new location value */ public void setLocation(final EquipmentLocation newLocation) { if (EquipmentLocation.CONTAINED.equals(newLocation)) { location = EquipmentLocation.CARRIED_NEITHER; } else { location = newLocation; } equipped = location.isEquipped(); } /** * OwnedItem Gets the hand attribute of the Equipment object * * @return EquipmentLocation containing the location value */ public EquipmentLocation getLocation() { return location; } /** * Get maximum charges * * @return maximum charges */ public int getMaxCharges() { return getEqModifierList(true).stream().map(eqMod -> eqMod.get(IntegerKey.MAX_CHARGES)) .filter(max -> max != null && max > 0).findFirst().orElse(0); } /** * Get minimum charges * * @return minimum charges */ public int getMinCharges() { return getEqModifierList(true).stream().map(eqMod -> eqMod.get(IntegerKey.MIN_CHARGES)) .filter(Objects::nonNull).findFirst().orElse(0); } /** * Set the name (sets keyname also) * * @param aString The new name */ @Override public void setName(final String aString) { super.setName(aString); setDirty(true); } /** * Sets the modifiedName attribute of the Equipment object * * @param nameString * The new modifiedName value */ public void setModifiedName(final String nameString) { modifiedName = nameString; setDirty(true); } /** * Sets the moveString attribute of the Equipment object * * @param aString * The new moveString value */ private void setMoveString(final String aString) { moveString = aString; } /** * Gets the name attribute of the Equipment object. Note * this is separate from toStirng to avoid side effects on keys. * * @return The name value */ public String getName() { final StringBuilder buffer = new StringBuilder(100); buffer.append(getDisplayName()); if (!modifiedName.isEmpty()) { buffer.append(" (").append(modifiedName).append(")"); } return buffer.toString(); } /** * set's the player added note for this item * * @param aString the value of the note */ public void setNote(final String aString) { noteString = aString; } /** * return the player added note for this item * * @return note */ public String getNote() { return noteString; } /** * Sets the numberCarried attribute of the Equipment object. * * @param aNumber * The new numberCarried value */ public void setNumberCarried(final Float aNumber) { carried = aNumber; } /** * Sets the numberEquipped attribute of the Equipment object. * @param num * The new numberEquipped value */ public void setNumberEquipped(final int num) { numberEquipped = num; if (num > 0) { equipped = true; } } /** * Gets the numberEquipped attribute of the Equipment object. * * @return The numberEquipped value */ public int getNumberEquipped() { return numberEquipped; } /** * Set this item's output index, which controls the order in which the * equipment appears on a character sheet. Note: -1 means hidden and 0 means * not set <p> <br> * author: James Dempsey 17-Jun-02 * * @param newIndex * the new output index for this equipment item (-1=hidden, 0=not * set) */ public void setOutputIndex(final int newIndex) { outputIndex = newIndex; } /** * Return the output index, which controls the order in which the equipment * appears on a character sheet. Note: -1 means hidden and 0 means not set * <p> <br> * author: James Dempsey 17-Jun-02 * * @return the output index for this equipment item (-1=hidden, 0=not set) */ public int getOutputIndex() { return outputIndex; } /** * Set this item's output subindex, which controls the order in which * equipment with the same output index appears on a character sheet. This * basically applies to natural weapons only, since they have output index 0 * <p> <br> * author: Stefan Radermacher 11-Feb-05 * * @param newSubindex * the new output subindex for this equipment item */ public void setOutputSubindex(final int newSubindex) { outputSubindex = newSubindex; } /** * Return the output subindex, which controls the order in which equipment * with the same output index appears on a character sheet. This basically * applies to natural weapons only, since they have output index 0 <p> <br> * author: Stefan Radermacher 11-Feb-05 * * @return the output subindex for this equipment item */ public int getOutputSubindex() { return outputSubindex; } /** * Sets the parent attribute of the Equipment object * * @param parent * The new parent value */ public void setParent(final Equipment parent) { d_parent = parent; } /** * Gets the parent of the Equipment object * * @return The parent */ public Equipment getParent() { return d_parent; } /** * Gets the parentName of the Equipment object * * @return The parentName */ public String getParentName() { final Equipment anEquip = getParent(); if (anEquip != null) { return anEquip.toString(); } if (isEquipped()) { return "Equipped"; } if (numberCarried().intValue() > 0) { return "Carried"; } return ""; } /** * Callback function from PObject.passesPreReqToGainForList() * * @param aType The string to be tested for PRETYPEness * PRETYPE:EQMODTYPE=MagicalEnhancement * PRETYPE:[EQMOD=Holy],EQMOD=WEAP+5 * PRETYPE:.IF.TYPE=Armor.Shield.Weapon.THEN.EQMODTYPE=MagicalEnhancement.ELSE. * * @return true if the Equipment's types match the aType string */ public boolean isPreType(String aType) { String tString = aType; // PRETYPE:EQMODTYPE=MagicalEnhancement // PRETYPE:[EQMOD=Holy],EQMOD=WEAP+5 // PRETYPE:.IF.TYPE=Armor.Shield.Weapon.THEN.EQMODTYPE=MagicalEnhancement.ELSE. if (tString.startsWith(".IF.TYPE=")) { final StringTokenizer aTok = new StringTokenizer(tString.substring(9), "."); int idx = tString.indexOf(".THEN."); if (idx < 0) { return false; } String truePart = tString.substring(idx + 6); int idx1 = truePart.indexOf(".ELSE."); String falsePart = ""; if (idx1 >= 0) { falsePart = truePart.substring(idx1 + 6); truePart = truePart.substring(0, idx1); } boolean typeFound = false; while (aTok.hasMoreTokens()) { final String aString = aTok.nextToken(); if (isType(aString, bonusPrimary)) { typeFound = true; break; } } if (typeFound) { tString = truePart; } else { tString = falsePart; } if (tString.isEmpty()) { return true; } } if (tString.startsWith("EQMODTYPE=") || tString.startsWith("EQMODTYPE.")) { tString = tString.substring(10); for (EquipmentModifier eqMod : getEqModifierList(bonusPrimary)) { if (eqMod.isType(tString)) { return true; } } return false; } else if (tString.startsWith("EQMOD=") || tString.startsWith("EQMOD.")) { String key = tString.substring(6); String choice = ""; if (key.indexOf('(') > 0) { int i = key.indexOf('('); choice = key.substring(i + 1, key.lastIndexOf(')')); key = key.substring(0, i); } EquipmentModifier eqMod = getEqModifierKeyed(key, bonusPrimary); if (eqMod != null) { if (StringUtils.isEmpty(choice)) { return true; } return (hasAssociations(eqMod) && choice.equalsIgnoreCase(getFirstAssociation(eqMod))); } return false; } return isType(tString, bonusPrimary); } /** * Sets the qty attribute of the Equipment object * * @param aString * The new qty value */ public void setQty(final String aString) { try { setQty(Double.parseDouble(aString)); } catch (NumberFormatException nfe) { qty = 0.0; } } /** * Sets the qty attribute of the Equipment object * * @param aFloat * The new qty value */ public void setQty(final Float aFloat) { setQty(aFloat.doubleValue()); } /** * Get the quantity of items * * @return return a Float of the quantity */ public Float getQty() { return new Float(qty); } /** * Gets the rawCritRange attribute of the Equipment object * * @param bPrimary * True=Primary Head * @return The rawCritRange value * @deprecated due to CRITRANGE code control */ @Deprecated public int getRawCritRange(final boolean bPrimary) { int range = getHeadInfo(bPrimary ? 1 : 2, IntegerKey.CRIT_RANGE); if (range == 0) { String cr = getWeaponInfo("CRITRANGE", bPrimary); if (!cr.isEmpty()) { try { range = Integer.parseInt(cr); } catch (NumberFormatException ignore) { //ignore } } } return range; } /** * Get the raw special properties * * @return raw special propertie */ @Override public String getRawSpecialProperties() { //CONSIDER standardize this with other joins? final StringBuilder retString = new StringBuilder(200); boolean first = true; for (SpecialProperty sprop : getSafeListFor(ListKey.SPECIAL_PROPERTIES)) { if (!first) { retString.append(", "); } first = false; retString.append(sprop.getText()); } return retString.toString(); } /** * Set the remaining charges * * @param remainingCharges The number of charges remaining */ public void setRemainingCharges(final int remainingCharges) { for (EquipmentModifier eqMod : getEqModifierList(true)) { Integer min = eqMod.get(IntegerKey.MIN_CHARGES); if (min != null && min > 0) { EqModSpellInfo.setRemainingCharges(this, eqMod, remainingCharges); } } } /** * Get the remaining charges * * @return remaining charges */ public int getRemainingCharges() { for (EquipmentModifier eqMod : getEqModifierList(true)) { Integer min = eqMod.get(IntegerKey.MIN_CHARGES); if (min != null && min > 0) { return EqModSpellInfo.getRemainingCharges(this, eqMod); } } return -1; } /** * Gets the simple name attribute of the Equipment object * * @return The name value */ public String getSimpleName() { return getDisplayName(); } /** * Gets the size attribute of the Equipment object * * @return The size value */ public String getSize() { return getSafe(ObjectKey.SIZE).get().getKeyName(); } public SizeAdjustment getSizeAdjustment() { return getSafe(ObjectKey.SIZE).get(); } /** * The number of "Slots" that this item requires The slot type is derived * from system/special/equipmentslot.lst * * @param aPC the PC with the Equipment * @return slots */ public int getSlots(final PlayerCharacter aPC) { int iSlots = getSafe(IntegerKey.SLOTS); EquipmentHead head = getEquipmentHeadReference(1); if (head != null) { for (EquipmentModifier eqMod : head.getSafeListFor(ListKey.EQMOD)) { iSlots += (int) eqMod.bonusTo(aPC, "EQM", "HANDS", this); iSlots += (int) eqMod.bonusTo(aPC, "EQM", "SLOTS", this); } } if (iSlots < 0) { iSlots = 0; } return iSlots; } public String getSlot() { return SystemCollections.getUnmodifiableEquipSlotList().stream().filter(es -> es.canContainType(getType())) .findFirst().map(EquipSlot::getSlotName).orElse(null); } /** * Returns special properties of an Equipment. * * @param aPC The PC with the Equipment * @return special properties of an Equipment. */ public String getSpecialProperties(final PlayerCharacter aPC) { final List<EquipmentModifier> modList; EquipmentHead head = getEquipmentHeadReference(1); if (head == null) { modList = Collections.emptyList(); } else { modList = head.getSafeListFor(ListKey.EQMOD); } EquipmentHead althead = getEquipmentHeadReference(2); final List<EquipmentModifier> altModList; if (althead == null) { altModList = Collections.emptyList(); } else { altModList = althead.getSafeListFor(ListKey.EQMOD); } final List<EquipmentModifier> comn = new ArrayList<>(); extractListFromCommon(comn, modList); removeCommonFromList(altModList, comn, "SPROP: eqMod expected but not found: "); final String common = StringUtil.join(getSpecialAbilityTimesList(getSpecialAbilityList(comn, aPC)), ", "); final String saList1 = StringUtil.join(getSpecialAbilityTimesList(getSpecialAbilityList(modList, aPC)), ", "); final String saList2 = StringUtil.join(getSpecialAbilityTimesList(getSpecialAbilityList(altModList, aPC)), ", "); final StringBuilder sp = new StringBuilder(200); boolean first = true; for (SpecialProperty sprop : getSafeListFor(ListKey.SPECIAL_PROPERTIES)) { final String text = sprop.getParsedText(aPC, this, this); if (!"".equals(text)) { if (!first) { sp.append(", "); } first = false; sp.append(text); } } if (!common.isEmpty()) { if (!first) { sp.append(", "); } first = false; sp.append(common); } if (!saList1.isEmpty()) { if (!first) { sp.append(", "); } first = false; if (isDouble()) { sp.append("Head1: "); } sp.append(saList1); } if (isDouble() && (!saList2.isEmpty())) { if (!first) { sp.append(", "); } sp.append("Head2: ").append(saList2); } return sp.toString(); } /** * Gets the uberParent attribute of the Equipment object * * @return The uberParent value */ public Equipment getUberParent() { if (getParent() == null) { return this; } Equipment anEquip = getParent(); while (anEquip.getParent() != null) { anEquip = anEquip.getParent(); } return anEquip; } /** * Get used charges * * @return used charges */ public int getUsedCharges() { for (EquipmentModifier eqMod : getEqModifierList(true)) { Integer min = eqMod.get(IntegerKey.MIN_CHARGES); if (min != null && min > 0) { return EqModSpellInfo.getUsedCharges(this, eqMod); } } return -1; } /** * Get the value of a variable passed as aString. This uses a different * variable processor than Player character because equipment has different * "hard coded" variables than a Player Character. * * @param varName * The name of the variable to look up * @param src The Source of the variable * @param aPC * The PC this equipment is associated with * * @return the value of the variable */ @Override public Float getVariableValue(final String varName, final String src, final PlayerCharacter aPC) { return getVariableValue(varName, src, bonusPrimary, aPC); } /** * Get the value of a variable passed as aString. This uses a different * variable processor than Player character because equipment has different * "hard coded" variables than a Player Character. * * @param varName * The name of the variable to look up * @param src The Source of the variable * @param bPrimary * If the head of the weapon has any effect on the variable * value, this flag stipulates which head to use (true means use * the primary head). * @param aPC * The PC this equipment is associated with * * @return The value of the variable */ public Float getVariableValue(String varName, final String src, final boolean bPrimary, final PlayerCharacter aPC) { VariableProcessor vp = new VariableProcessorEq(this, aPC, bPrimary); return vp.getVariableValue(null, varName, src, 0); } /** * Returns true if the equipment modifier is visible * * @param eqMod * The equipment modifier * @return The visible value */ public boolean isVisible(final EquipmentModifier eqMod, View v) { Visibility vis = eqMod.getSafe(ObjectKey.VISIBILITY); if (Visibility.QUALIFY.equals(vis)) { bonusPrimary = true; if (PrereqHandler.passesAll(eqMod, this, null)) { return true; } // // Check the secondary head if the primary head doesn't qualify (and // the item has a secondary head) // if (isDouble()) { bonusPrimary = false; return PrereqHandler.passesAll(eqMod, this, null); } return false; } return vis.isVisibleTo(v); } /** * Returns true if the equipment modifier is visible * * @param eqMod * The equipment modifier * @param primaryHead * Is this for the main head (true), or the secondary one (false)? * @return The visible value */ public boolean isVisible(PlayerCharacter pc, EquipmentModifier eqMod, boolean primaryHead, View v) { Visibility vis = eqMod.getSafe(ObjectKey.VISIBILITY); if (Visibility.QUALIFY.equals(vis)) { bonusPrimary = primaryHead; return PrereqHandler.passesAll(eqMod, this, pc); } return vis.isVisibleTo(v); } /** * Gets the weight attribute of the Equipment object. * * @param aPC The PC that has this Equipment * * @return The weight value */ public Float getWeight(final PlayerCharacter aPC) { if (virtualItem) { return new Float(0.0); } return new Float(getWeightAsDouble(aPC)); } /** * get base weight as double * * @return base weight (as a double) */ private BigDecimal getBaseWeight() { if (this.isVirtual()) { return BigDecimal.ZERO; } return getWeightInPounds().add(getSafe(ObjectKey.WEIGHT_MOD)); } /** * Get the weight as a double * * @param aPC The PC that has this Equipment * @return weight as double */ public double getWeightAsDouble(final PlayerCharacter aPC) { if (isVirtual()) { return 0.0; } double d1 = bonusTo(aPC, "EQM", "WEIGHTMULT", true); double aWeight = getWeightInPounds().doubleValue(); if (!CoreUtility.doublesEqual(d1, 0.0)) { aWeight *= d1; } double d2 = bonusTo(aPC, "EQM", "WEIGHTDIV", true); if (!CoreUtility.doublesEqual(d2, 0)) { aWeight /= d2; } aWeight += bonusTo(aPC, "EQM", "WEIGHTADD", true); aWeight += getSafe(ObjectKey.WEIGHT_MOD).doubleValue(); return aWeight; } /** * Get weild * * @return weild */ public String getWieldName() { WieldCategory wield = get(ObjectKey.WIELD); return wield == null ? "" : wield.getKeyName(); } /** * Description of the Method * * @param aPC The PC that has this Equipment * * @return Description of the Return Value * @deprecated due to ACCHECK code control */ @Deprecated public Integer preFormulaAcCheck(final PlayerCharacter aPC) { return Math.min(getSafe(IntegerKey.AC_CHECK) + (int) bonusTo(aPC, "EQMARMOR", "ACCHECK", true), 0); } /** * Returns true if the Equipment can take children. * * @return true if the Equipment can take children. */ public boolean acceptsChildren() { return get(ObjectKey.CONTAINER_WEIGHT_CAPACITY) != null; } /** * Add an equipment modifier and its associated information eg: * Bane|Vermin|Fey eg: Keen Adds a feature to the EqModifier attribute of * the Equipment object * * @param aString * The feature to be added to the EqModifier attribute * @param bPrimary * The feature to be added to the EqModifier attribute * @param isLoading Is the equipment item being loaded currently. */ private void addEqModifier(final String aString, final boolean bPrimary, final boolean isLoading) { final StringTokenizer aTok = new StringTokenizer(aString, "|"); // The type of EqMod, eg: ABILITYPLUS final String eqModKey = aTok.nextToken(); EquipmentModifier eqMod = getEqModifierKeyed(eqModKey, bPrimary); // If not already attached, then add a new one if (eqMod == null) { if (eqModKey.equals(EQMOD_WEIGHT)) { if (aTok.hasMoreTokens()) { put(ObjectKey.WEIGHT_MOD, new BigDecimal(aTok.nextToken().replace(',', '.'))); } return; } if (eqModKey.equals(EQMOD_DAMAGE)) { if (aTok.hasMoreTokens()) { put(StringKey.DAMAGE_OVERRIDE, aTok.nextToken()); } return; } eqMod = Globals.getContext().getReferenceContext() .silentlyGetConstructedCDOMObject(EquipmentModifier.class, eqModKey); if (eqMod == null) { Logging.errorPrint("Could not find EquipmentModifier: " + eqModKey); return; } // only make a copy if we need to // add qualifiers to modifier if (!eqMod.getSafe(StringKey.CHOICE_STRING).isEmpty()) { eqMod = eqMod.clone(); } addToEqModifierList(eqMod, bPrimary); } // Add the associated choices if (!eqMod.getSafe(StringKey.CHOICE_STRING).isEmpty()) { while (aTok.hasMoreTokens()) { final String x = aTok.nextToken(); Integer min = eqMod.get(IntegerKey.MIN_CHARGES); if (min != null && min > 0 || (eqMod.getSafe(StringKey.CHOICE_STRING).startsWith("EQBUILDER") && !isLoading)) { // We clear the associated info to avoid a buildup of info // like number of charges. removeAllAssociations(eqMod); } addAssociation(eqMod, x.replace('=', '|')); } } } /** * Adds a feature to the EqModifier attribute of the Equipment object. If a * choice is required, a dialog will be displayed asking the user for the * choice. * * @param eqMod * The feature to be added to the EqModifier attribute * @param bPrimary * The feature to be added to the EqModifier attribute * @param aPC * The PC that the modifier is being added for. */ public void addEqModifier(final EquipmentModifier eqMod, final boolean bPrimary, final PlayerCharacter aPC) { addEqModifier(eqMod, bPrimary, aPC, null, null); } /** * Adds a feature to the EqModifier attribute of the Equipment object. If a * non-null selectedChoice is supplied, this method will not be interactive, * and will not show a dialog if a choice is required. Instead, the provided * value will be used. * * @param eqMod * The feature to be added to the EqModifier attribute * @param bPrimary * The feature to be added to the EqModifier attribute * @param aPC * The PC that the modifier is being added for. * @param selectedChoice * The choice to be used instead of asking the user, should a * choice be required. * @param equipChoice * The details of the choice to be made. Used when there are * secondary options. */ void addEqModifier(final EquipmentModifier eqMod, final boolean bPrimary, final PlayerCharacter aPC, final String selectedChoice, final EquipmentChoice equipChoice) { boolean bImporting = false; if ((aPC != null) && aPC.isImporting()) { bImporting = true; } if (!bImporting && !canAddModifier(aPC, eqMod, bPrimary)) { return; } List<CDOMSingleRef<EquipmentModifier>> replaces = eqMod.getListFor(ListKey.REPLACED_KEYS); EquipmentHead head = getEquipmentHead(bPrimary ? 1 : 2); if (replaces != null) { // // Remove any modifiers that this one will replace // replaces.stream().map(CDOMSingleRef::get).map(CDOMObject::getKeyName) .forEach(key -> head.getSafeListFor(ListKey.EQMOD).stream() .filter(aMod -> key.equalsIgnoreCase(aMod.getKeyName())).forEach(aMod -> { head.removeFromListFor(ListKey.EQMOD, aMod); if (bPrimary) { usePrimaryCache = false; } else { useSecondaryCache = false; } setDirty(true); })); } if (eqMod.isType("BaseMaterial")) { head.getSafeListFor(ListKey.EQMOD).stream().filter(aMod -> aMod.isType("BaseMaterial")) .forEach(aMod -> { head.removeFromListFor(ListKey.EQMOD, aMod); if (bPrimary) { usePrimaryCache = false; } else { useSecondaryCache = false; } setDirty(true); }); } else if (eqMod.isType("MagicalEnhancement")) { head.getSafeListFor(ListKey.EQMOD).stream().filter(aMod -> aMod.isType("MagicalEnhancement")) .forEach(aMod -> { head.removeFromListFor(ListKey.EQMOD, aMod); if (bPrimary) { usePrimaryCache = false; } else { useSecondaryCache = false; } }); } // // Add the modifier if it's not already there // EquipmentModifier aMod = getEqModifierKeyed(eqMod.getKeyName(), bPrimary); if (aMod == null) { // // only make a copy if we need to add qualifiers to modifier // if (eqMod.getSafe(StringKey.CHOICE_STRING).isEmpty()) { aMod = eqMod; } else { aMod = eqMod.clone(); if (aMod == null) { return; } } addToEqModifierList(aMod, bPrimary); } // // If a choice is required, either get a response from user or // apply the provided choice. // Remove the modifier if all associated choices are deleted // if (!bImporting) { boolean allRemoved = false; if (selectedChoice != null && !selectedChoice.isEmpty()) { if (!eqMod.getSafe(StringKey.CHOICE_STRING).startsWith("EQBUILDER.")) { EquipmentChoiceDriver.setChoice(this, aMod, selectedChoice, equipChoice); allRemoved = !hasAssociations(aMod); } } else if (!EquipmentChoiceDriver.getChoice(1, this, aMod, true, aPC)) { allRemoved = true; } if (allRemoved) { head.removeFromListFor(ListKey.EQMOD, aMod); if (bPrimary) { usePrimaryCache = false; } else { useSecondaryCache = false; } } } setBase(); } /** * Add a list equipment modifiers and their associated information eg: * Bane|Vermin|Fey.Keen.Vorpal.ABILITYPLUS|CHA=+6 <p> Adds a feature to the * EqModifiers attribute of the Equipment object * * @param aString * The feature to be added to the EqModifiers attribute * @param bPrimary * The feature to be added to the EqModifiers attribute */ public void addEqModifiers(final String aString, final boolean bPrimary) { addEqModifiers(aString, bPrimary, false); } /** * Add a list equipment modifiers and their associated information eg: * Bane|Vermin|Fey.Keen.Vorpal.ABILITYPLUS|CHA=+6 <p> Adds a feature to the * EqModifiers attribute of the Equipment object * * @param aString * The feature to be added to the EqModifiers attribute * @param bPrimary * The feature to be added to the EqModifiers attribute * @param isLoading Is the equipment item being loaded currently. */ private void addEqModifiers(final String aString, final boolean bPrimary, final boolean isLoading) { final StringTokenizer aTok = new StringTokenizer(aString, "."); while (aTok.hasMoreTokens()) { final String aEqModName = aTok.nextToken(); if (!aEqModName.equalsIgnoreCase(Constants.NONE)) { addEqModifier(aEqModName, bPrimary, isLoading); } } } /** * Description of the Method * * @param aPC * The PC that has this Equipment * @param aType * a TYPE of BONUS (such as "COMBAT" or "AC") * @param aName * the NAME of the BONUS (such as "ATTACKS" or "ARMOR") * @param bPrimary * should we ask the parent object also? * @return returns a double which is the sum of all bonuses */ public double bonusTo(final PlayerCharacter aPC, final String aType, final String aName, final boolean bPrimary) { return bonusTo(aPC, aType, aName, this, bPrimary); } /** * Add bonuses * * @param aPC * The PC that has this Equipment * @param aType * The type of the Bonus * @param aName * The name of the Bonus * @param anObj * An object used in the bonus calculations, should be a * PC or a piece of Equipment. * @param bPrimary * If true get bonuses for primary head * @return bonus */ private double bonusTo(final PlayerCharacter aPC, final String aType, final String aName, final Object anObj, final boolean bPrimary) { StringBuilder sB = new StringBuilder(aType.toUpperCase()); sB.append('.'); sB.append(aName.toUpperCase()); sB.append('.'); final String aBonusKey = sB.toString(); // go through bonus hashmap and zero out all // entries that deal with this bonus request getBonusMap().keySet().stream().filter(aKey -> aKey.startsWith(aBonusKey)) .forEach(aKey -> putBonusMap(aKey, "0")); bonusPrimary = bPrimary; if (bPrimary) { BonusCalc.equipBonusTo(this, aType, aName, aPC); // now do temp bonuses final List<BonusObj> tbList = getTempBonusList().stream().distinct().collect(Collectors.toList()); BonusCalc.bonusTo(this, aType, aName, anObj, tbList, aPC); } // If using 3.5 weapon penalties, add them in also if (Globals.checkRule(RuleConstants.SYS_35WP)) { for (EqSizePenalty esp : Globals.getContext().getReferenceContext() .getConstructedCDOMObjects(EqSizePenalty.class)) { BonusCalc.bonusTo(this, aType, aName, this, esp.getBonuses(), aPC); } } final List<EquipmentModifier> eqModList = getEqModifierList(bPrimary); for (EquipmentModifier eqMod : eqModList) { eqMod.bonusTo(aPC, aType, aName, this); } double iBonus = getBonusMap().keySet().stream().filter(key -> key.startsWith(aBonusKey)) .mapToDouble(key -> Float.parseFloat(getBonusMap().get(key))).sum(); return iBonus; } /** * Calculates the plus value fo the specified head * * @param bPrimary Which head is required, the primary (true) or the secondary (false) * @return The plus for the equipment head */ public int calcPlusForHead(boolean bPrimary) { int iPlus = 0; int headnum = bPrimary ? 1 : 2; EquipmentHead head = getEquipmentHeadReference(headnum); if (head == null) { return iPlus; } for (EquipmentModifier eqMod : head.getSafeListFor(ListKey.EQMOD)) { int iCount = getSelectCorrectedAssociationCount(eqMod); if (iCount < 1) { iCount = 1; } iPlus += (iCount * eqMod.getSafe(IntegerKey.PLUS)); } return iPlus; } /** * Can we add eqMod to this equipment? * * @param eqMod * The Equipment Modifier we would like to add * @param bPrimary * whether adding to the primary or secondary head * * @return True if eqMod is addable */ public boolean canAddModifier(PlayerCharacter pc, PrereqObject eqMod, boolean bPrimary) { // Make sure we are qualified bonusPrimary = bPrimary; return getSafe(ObjectKey.MOD_CONTROL).getModifiersAllowed() && PrereqHandler.passesAll(eqMod, this, pc); } /** * Returns 0 on object error, 1 on can fit, 2 on too heavy, 3 on properties * problem (unimplemented), 4 on capacity error * * @param aPC * The PC that has the Equipment * @param obj * The equipment to check * @return 0 on object error, 1 on can fit, 2 on too heavy, 3 on properties * problem (unimplemented), 4 on capacity error */ public int canContain(final PlayerCharacter aPC, final Object obj) { if (obj instanceof Equipment) { final Equipment anEquip = (Equipment) obj; Float f = new Float(anEquip.getWeightAsDouble(aPC) * anEquip.numberCarried()); if (checkChildWeight(aPC, f)) { // canHold(my HashMap())) //quick hack since the properties // hashmap doesn't exist if (checkContainerCapacity(anEquip.eqTypeList(), anEquip.numberCarried())) { // the qty value is a temporary hack - insert all or // nothing. should reset person to be a container, with // capacity=capacity return 1; } return 4; } return 2; } return 0; } /** * Description of the Method * * @return Description of the Return Value */ @Override public Equipment clone() { Equipment eq = null; try { eq = (Equipment) super.clone(); eq.heads = new ArrayList<>(); for (EquipmentHead head : heads) { if (head == null) { eq.heads.add(null); } else { EquipmentHead eh = new EquipmentHead(eq, head.getHeadIndex()); eh.overlayCDOMObject(head); eq.heads.add(eh); } } // if (bonusMap != null) { eq.bonusMap = new HashMap<>(bonusMap); } eq.setMoveString(moveString()); // eq.setTypeString(super.getType()); // none of the types associated with modifiers eq.carried = carried; eq.equipped = equipped; eq.location = location; eq.numberEquipped = numberEquipped; eq.qty = qty; eq.outputIndex = outputIndex; eq.d_childTypes = new HashMap<>(d_childTypes); eq.d_containedEquipment = new ArrayList<>(d_containedEquipment); eq.assocSupt = assocSupt.clone(); eq.getEquipmentHead(1).removeListFor(ListKey.EQMOD); eq.getEquipmentHead(2).removeListFor(ListKey.EQMOD); eq.getEquipmentHead(1).addAllToListFor(ListKey.EQMOD, cloneEqModList(eq, true)); eq.getEquipmentHead(2).addAllToListFor(ListKey.EQMOD, cloneEqModList(eq, false)); } catch (CloneNotSupportedException e) { ShowMessageDelegate.showMessageDialog(e.getMessage(), Constants.APPLICATION_NAME, MessageType.ERROR); } return eq; } /** * Description of the Method * * @param o * Description of the Parameter * @return Description of the Return Value */ @Override public int compareTo(final Object o) { final Equipment e = (Equipment) o; return getName().compareToIgnoreCase(e.getName()); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!super.equals(obj)) { return false; } if (getClass() != obj.getClass()) { return false; } Equipment other = (Equipment) obj; String displayName = getDisplayName(); if (displayName == null) { if (other.getDisplayName() != null) { return false; } } else if (!displayName.equals(other.getDisplayName())) { return false; } if (modifiedName == null) { if (other.modifiedName != null) { return false; } } else if (!modifiedName.equals(other.modifiedName)) { return false; } return true; } /** * Build a String used to save this items special properties in a .pcg file * * @param sep used to separate the items in the string * @param endPart * used as a separatot between the label and the data for * each item in the string * @return String */ public String formatSaveLine(final char sep, final char endPart) { final StringBuilder sbuf = new StringBuilder(100); final Equipment base; CDOMSingleRef<Equipment> baseItem = get(ObjectKey.BASE_ITEM); if (baseItem == null) { base = this; sbuf.append(getBaseItemName()); } else { base = baseItem.get(); sbuf.append(base.getKeyName()); sbuf.append(sep).append("NAME").append(endPart).append(toString(false)); } // When you customise a piece of equipment using the customiser, it sets // the keyName equal to the Name. The autoresizer doesn't do that, it // makes a new key. This is to cope with the auto resizer. if (!this.getKeyName().equals(this.getName())) { sbuf.append(sep).append("KEY").append(endPart).append(this.getKeyName()); } SizeAdjustment thisSize = getSafe(ObjectKey.SIZE).get(); if (!thisSize.equals(base.getSafe(ObjectKey.SIZE).get())) { sbuf.append(sep).append("SIZE").append(endPart).append(thisSize.getKeyName()); } String string1 = getEqModifierString(true); // key1.key2|assoc1|assoc2.key3.key4 if (!string1.isEmpty()) { sbuf.append(sep).append("EQMOD").append(endPart).append(string1); } String string2 = getEqModifierString(false); // key1.key2|assoc1|assoc2.key3.key4 if (!string2.isEmpty()) { sbuf.append(sep).append("ALTEQMOD").append(endPart).append(string2); } String string3 = getRawSpecialProperties(); if ((!string3.isEmpty()) && !string3.equals(base.getRawSpecialProperties())) { sbuf.append(sep).append("SPROP").append(endPart).append(string3); } if (!costMod.equals(BigDecimal.ZERO)) { sbuf.append(sep).append("COSTMOD").append(endPart).append(costMod.toString()); } return sbuf.toString(); } @Override public int hashCode() { final int prime = 31; int result = 1; String displayName = getDisplayName(); result = prime * result + ((displayName == null) ? 0 : displayName.hashCode()); result = prime * result + ((modifiedName == null) ? 0 : modifiedName.hashCode()); return result; } /** * Gets the index of a child * * @param child * The child * @return the index of the child */ private int indexOfChild(final Object child) { if (!(child instanceof Equipment)) { return -1; } return getContainedEquipmentIndexOf((Equipment) child); } /** * Adds a child to this Equipment * * @param aPC The PC that has the Equipment * @param child * The child to add */ public void insertChild(final PlayerCharacter aPC, final Object child) { if (child == null) { return; } Equipment anEquip = (Equipment) child; Float aFloat = anEquip.numberCarried(); Float bFloat = aFloat; final String aString = pickChildType(anEquip.eqTypeList(), aFloat); if (containsChildType(aString)) { aFloat = getChildType(aString) + aFloat; } bFloat = getChildType("Total") + bFloat; setChildType(aString, aFloat); setChildType("Total", bFloat); addContainedEquipment(anEquip); anEquip.setIndexedUnderType(aString); anEquip.setParent(this); // hmm probably not needed; but as it currently isn't hurting // anything... updateContainerContentsString(aPC); while (anEquip.getParent() != null) { anEquip = anEquip.getParent(); anEquip.updateContainerContentsString(aPC); } } /** * Returns how 'deep' in a structure an Equipment is. * * @return how 'deep' in a structure an Equipment is. */ public int itemDepth() { if (getParent() == null) { return 0; } int i = 1; Equipment anEquip = getParent(); while (anEquip.getParent() != null) { anEquip = anEquip.getParent(); ++i; } return i; } /** * load a "line" i.e. a String and use its data to populate the attributes * of this Equipment * * @param aLine * The data to parse * @param sep * The item separator used in the data * @param endPart * The separator used between a label and its associated data * @param aPC * The PC used to size the Equipment (may be null) */ public void load(final String aLine, final String sep, final String endPart, final PlayerCharacter aPC) { final StringTokenizer aTok = new StringTokenizer(aLine, sep); final int endPartLen = endPart.length(); CDOMSingleRef<SizeAdjustment> size = getSafe(ObjectKey.SIZE); boolean firstSprop = true; while (aTok.hasMoreTokens()) { final String aString = aTok.nextToken(); if (aString.startsWith("NAME" + endPart)) { setName(aString.substring(4 + endPartLen).intern()); put(StringKey.OUTPUT_NAME, getDisplayName()); } else if (aString.startsWith("KEY" + endPart)) { put(StringKey.KEY_NAME, aString.substring(3 + endPartLen)); } else if (aString.startsWith("SIZE" + endPart)) { size = Globals.getContext().getReferenceContext().getCDOMReference(SizeAdjustment.class, aString.substring(4 + endPartLen)); } else if (aString.startsWith("EQMOD" + endPart)) { addEqModifiers(aString.substring(5 + endPartLen), true, true); } else if (aString.startsWith("ALTEQMOD" + endPart)) { addEqModifiers(aString.substring(8 + endPartLen), false); } else if (aString.startsWith("SPROP" + endPart)) { if (firstSprop) { removeListFor(ListKey.SPECIAL_PROPERTIES); firstSprop = false; } addToListFor(ListKey.SPECIAL_PROPERTIES, SpecialProperty.createFromLst(aString.substring(5 + endPartLen))); } else if (aString.startsWith("COSTMOD" + endPart)) { setCostMod(aString.substring(7 + endPartLen)); } else if (aString.startsWith("WEIGHTMOD" + endPart)) { put(ObjectKey.WEIGHT_MOD, new BigDecimal(aString.substring(9 + endPartLen))); } } put(ObjectKey.CUSTOMSIZE, size); } /** * Sets this Equipment to the size defined in ObjectKey.CUSTOMSIZE. This * should be done after equipment load but before use of the Equipment. * * Note that this *should not* be done until full data load is complete to * ensure that there is not a race condition on resolving sizes. */ public void setToCustomSize(PlayerCharacter pc) { CDOMSingleRef<SizeAdjustment> csr = get(ObjectKey.CUSTOMSIZE); if (csr != null) { SizeAdjustment customSize = csr.get(); if (!getSafe(ObjectKey.SIZE).equals(customSize)) { resizeItem(pc, customSize); } } } /** * Get the long name of this piece of equipment * * @return the verbose name */ public String longName() { return toString(true); } /** * Is the PC qualified to use this equipment * * @param pc The PC to check the prerequisites against * * @return Description of the Return Value */ public boolean meetsPreReqs(PlayerCharacter pc) { return PrereqHandler.passesAll(this, this, pc); } /** * Get the modified name e.g. "Natural/Primary" of this Equipment. * Is mostly unset and (if set) is added to the display name when * producing the long name * * @return the modified name */ public String modifiedName() { return modifiedName; } /** * Process and return a movement string * * @return the Movement string */ public String moveString() { if (!moveString.isEmpty()) { final Load eqLoad; if (isHeavy()) { eqLoad = Load.HEAVY; } else if (isMedium()) { eqLoad = Load.MEDIUM; } else if (isLight()) { eqLoad = Load.LIGHT; } else { eqLoad = Load.OVERLOAD; } // // This will generate a list for base moves 30,20 // or 60,50,40 depending on how many tokens are // in the original tag // final StringTokenizer aTok = new StringTokenizer(moveString, ","); int baseMove = -1; int tokenCount = aTok.countTokens(); switch (tokenCount) { case 2: baseMove = 30; break; case 3: baseMove = 60; break; default: tokenCount = -1; break; } if (tokenCount > 0) { final StringBuilder retString = new StringBuilder(moveString.length()); for (int i = 0; i < tokenCount; ++i) { if (i != 0) { retString.append(','); } retString.append(Globals.calcEncumberedMove(eqLoad, baseMove)); baseMove -= 10; } return retString.toString(); } } return moveString; } /** * Generate a name from the Base Equipement name and any EqMods that * have been applied. * * @param pc the PC that has the equipment * * @return a name generated from the Base Equipement type and any EqMods applied */ public String nameItemFromModifiers(final PlayerCharacter pc) { final String itemName = getItemNameFromModifiers(getBaseItemName()); setDefaultCrit(pc); setName(itemName); String itemKey = getItemNameFromModifiers(getBaseItemKeyName()).replaceAll("[^A-Za-z0-9/_() +-]", "_"); setKeyName(itemKey); remove(StringKey.OUTPUT_NAME); return getKeyName(); } /** * Get the number of items of this Equipment being carried * * @return the number of this Equipment carried */ public Float numberCarried() { Equipment eqParent = getParent(); if (isEquipped() || (eqParent == null)) { return carried; } for (; eqParent != null; eqParent = eqParent.getParent()) { if (eqParent.isEquipped() || ((eqParent.getParent() == null) && (eqParent.numberCarried().intValue() != 0))) { return carried; } } return (float) 0; } /** * Get the quantity of items * * @return the quantity of items */ public double qty() { return qty; } /** * Removes a child from the Equipment * * @param pc * The PC carrying the item * * @param child * The child to remove */ public void removeChild(final PlayerCharacter pc, final Object child) { final int i = indexOfChild(child); Equipment anEquip = (Equipment) child; final Float qtyRemoved = anEquip.numberCarried(); setChildType("Total", getChildType("Total") - qtyRemoved); final String aString = anEquip.isIndexedUnderType(); setChildType(aString, getChildType(aString) - qtyRemoved); anEquip.setParent(null); removeContainedEquipment(i); updateContainerContentsString(pc); Equipment equipment = this; while (equipment.getParent() != null) { equipment = equipment.getParent(); equipment.updateContainerContentsString(pc); } } /** * Description of the Method * * @param eqMod * Description of the Parameter * @param bPrimary * Description of the Parameter * @param pc * The PC carrying the item */ public void removeEqModifier(final EquipmentModifier eqMod, final boolean bPrimary, PlayerCharacter pc) { final EquipmentModifier aMod = getEqModifierKeyed(eqMod.getKeyName(), bPrimary); if (aMod == null) { return; } // Get a response from user (if one required) // Remove the modifier if all associated choices are deleted if (!hasAssociations(aMod) || !EquipmentChoiceDriver.getChoice(0, this, aMod, false, pc)) { EquipmentHead head = getEquipmentHead(bPrimary ? 1 : 2); head.removeFromListFor(ListKey.EQMOD, aMod); if (bPrimary) { usePrimaryCache = false; } else { useSecondaryCache = false; } restoreEqModsAfterRemove(pc, eqMod, bPrimary, head); setDirty(true); } } /** * Add back in modifiers that this one previously removed. * * @param eqMod The equipment modifier being removed. * @param bPrimary Which head is this for? * @param head The head being updated. */ private void restoreEqModsAfterRemove(PlayerCharacter pc, final EquipmentModifier eqMod, final boolean bPrimary, EquipmentHead head) { CDOMSingleRef<Equipment> baseItem = get(ObjectKey.BASE_ITEM); if (baseItem == null) { return; } List<CDOMSingleRef<EquipmentModifier>> replaces = eqMod.getListFor(ListKey.REPLACED_KEYS); if (replaces != null) { // // Add back in modifiers that this one previously removed // replaces.stream().map(CDOMSingleRef::get).map(CDOMObject::getKeyName) .forEach(key -> baseItem.get().getEquipmentHead(bPrimary ? 1 : 2).getSafeListFor(ListKey.EQMOD) .stream().filter(baseMod -> key.equalsIgnoreCase(baseMod.getKeyName())) .forEach(baseMod -> { head.addToListFor(ListKey.EQMOD, baseMod); })); } if (eqMod.isType("BaseMaterial")) { baseItem.get().getEquipmentHead(bPrimary ? 1 : 2).getSafeListFor(ListKey.EQMOD).stream() .filter(baseMod -> baseMod.isType("BaseMaterial")).forEach(baseMod -> { head.addToListFor(ListKey.EQMOD, baseMod); }); } else if (eqMod.isType("MagicalEnhancement")) { baseItem.get().getEquipmentHead(bPrimary ? 1 : 2).getSafeListFor(ListKey.EQMOD).stream() .filter(baseMod -> baseMod.isType("MagicalEnhancement")).forEach(baseMod -> { head.addToListFor(ListKey.EQMOD, baseMod); }); } } /** * Remove a list equipment modifiers and their associated information eg: * Bane|Vermin|Fey.Keen.Vorpal.ABILITYPLUS|CHA=+6 <p> Removes a feature * from the EqModifiers attribute of the Equipment object * * @param aString * The feature to be removed from the EqModifiers attribute * @param bPrimary * The feature to be removed from the EqModifiers attribute * @param pc * The PC carrying the item */ public void removeEqModifiers(final String aString, final boolean bPrimary, PlayerCharacter pc) { final StringTokenizer aTok = new StringTokenizer(aString, "."); while (aTok.hasMoreTokens()) { final String aEqModName = aTok.nextToken(); if (!aEqModName.equalsIgnoreCase(Constants.NONE)) { removeEqModifier(aEqModName, bPrimary, pc); } } } /** * Change the size of an item * * @param pc * The PC carrying the item * @param newSize * The new size for the item */ public void resizeItem(final PlayerCharacter pc, SizeAdjustment newSize) { setBase(); final int iOldSize = sizeInt(); int iNewSize = newSize.get(IntegerKey.SIZEORDER); if (iNewSize != iOldSize) { put(ObjectKey.SIZE, CDOMDirectSingleRef.getRef(newSize)); CDOMSingleRef<Equipment> baseItem = get(ObjectKey.BASE_ITEM); Equipment eq; if (baseItem == null) { eq = this; } else { eq = baseItem.get(); } put(ObjectKey.CURRENT_COST, eq.getCostAdjustedForSize(pc, newSize)); put(ObjectKey.WEIGHT, eq.getWeightAdjustedForSize(pc, newSize)); adjustACForSize(pc, eq, newSize); String dam = eq.getDamageAdjustedForSize(newSize, true); if (dam != null && !dam.isEmpty()) { getEquipmentHead(1).put(StringKey.DAMAGE, dam); } String adam = eq.getDamageAdjustedForSize(newSize, false); if (adam != null && !adam.isEmpty()) { getEquipmentHead(2).put(StringKey.DAMAGE, adam); } // // Adjust the capacity of the container (if it is one) // BigDecimal weightCap = get(ObjectKey.CONTAINER_WEIGHT_CAPACITY); if (weightCap != null) { double mult = 1.0; if (newSize != null && pc != null) { mult = pc.getSizeBonusTo(newSize, "ITEMCAPACITY", eq.typeList(), 1.0); } BigDecimal multbd = new BigDecimal(mult); if (!Capacity.UNLIMITED.equals(weightCap)) { // CONSIDER ICK, ICK, direct access bad put(ObjectKey.CONTAINER_WEIGHT_CAPACITY, weightCap.multiply(multbd)); } List<Capacity> capacity = removeListFor(ListKey.CAPACITY); if (capacity != null) { for (Capacity cap : capacity) { BigDecimal content = cap.getCapacity(); if (!Capacity.UNLIMITED.equals(content)) { content = content.multiply(multbd); } // CONSIDER ICK, ICK, direct access bad addToListFor(ListKey.CAPACITY, new Capacity(cap.getType(), content)); } } updateContainerCapacityString(); } } // // Since we've just resized the item, we need to modify any PRESIZE // prerequisites // if (hasPrerequisites()) { AbstractReferenceContext ref = Globals.getContext().getReferenceContext(); int maxIndex = ref.getConstructedObjectCount(SizeAdjustment.class); for (Prerequisite aBonus : getPrerequisiteList()) { if ("SIZE".equalsIgnoreCase(aBonus.getKind())) { SizeAdjustment sa = ref.silentlyGetConstructedCDOMObject(SizeAdjustment.class, aBonus.getOperand()); final int iOldPre = sa.get(IntegerKey.SIZEORDER); iNewSize += (iOldPre - iOldSize); if ((iNewSize >= 0) && (iNewSize <= maxIndex)) { // Note: This actually impacts the Prereq in this // Equipment, since it is returned // by reference from the get above ... thus no need to // perform a set SizeAdjustment size = ref.getSortedList(SizeAdjustment.class, IntegerKey.SIZEORDER) .get(iNewSize); aBonus.setOperand(size.getKeyName()); } } } } } /** * Get the int size of the Equipment object * * @return size as int */ public int sizeInt() { SizeAdjustment size = getSafe(ObjectKey.SIZE).get(); return size.get(IntegerKey.SIZEORDER); } /** * Returns the Equipment as a String * * @return the Equipment as a String */ @Override public String toString() { return toString(true); } /** * Returns a String representation of the Equipment * * @param addCharges * if true include the number of charges the Item has in the * returned string * * @return the Equipment as a String */ private String toString(final boolean addCharges) { if (isDirty() || (cachedNameWithCharges == null && cachedNameWithoutCharges == null)) { // If we have modified the equipment details with // respect to the name then rebuid the names final StringBuilder buffer = new StringBuilder(100); if (SettingsHandler.guiUsesOutputNameEquipment()) { buffer.append(getOutputName()); } else { buffer.append(getDisplayName()); } if (!modifiedName.isEmpty()) { buffer.append(" (").append(modifiedName).append(")"); } cachedNameWithoutCharges = buffer.toString(); if (addCharges) { int rem = getRemainingCharges(); if ((rem > 0) && (rem < getMaxCharges())) { buffer.append("(").append(rem).append(")"); } } cachedNameWithCharges = buffer.toString(); setDirty(false); } // Return the cached names. if (addCharges) { return cachedNameWithCharges; } return cachedNameWithoutCharges; } private boolean isDirty() { return dirty; } private void setDirty(final boolean dirty) { this.dirty = dirty; } /** * Returns the type with the requested index * * @param index * the index * @return the type with the requested index */ public String typeIndex(final int index) { final List<String> tList = typeList(); if ((index < 0) || (index >= tList.size())) { return ""; } return tList.get(index); } /** * Returns a list of the types of this item. * * @return a list of the types of this item. */ public List<String> typeList() { return typeList(true); } /** * Updates the containerContentsString from children of this item * * @param pc The PC carrying the item */ void updateContainerContentsString(final PlayerCharacter pc) { final StringBuilder tempStringBuilder = new StringBuilder(getChildCount() * 20); // Make sure there's no bug here. if (pc != null && acceptsChildren() && (getContainedWeight(pc, true) >= 0.0f)) { tempStringBuilder .append(Globals.getGameModeUnitSet() .displayWeightInUnitSet(getContainedWeight(pc, true).doubleValue())) .append(Globals.getGameModeUnitSet().getWeightUnit()); } else { // have to put something tempStringBuilder.append("0.0 "); tempStringBuilder.append(Globals.getGameModeUnitSet().getWeightUnit()); } // karianna os bug 1414564 IntStream.range(0, getChildCount()).mapToObj(e -> (Equipment) getChild(e)) .filter(anEquip -> anEquip.getQty() > 0.0f).forEach(anEquip -> { tempStringBuilder.append(", "); tempStringBuilder.append(BigDecimalHelper.trimZeros(anEquip.getQty().toString())); tempStringBuilder.append(" "); tempStringBuilder.append(anEquip.getOutputName()); }); containerContentsString = tempStringBuilder.toString(); } /** * @param aPC The PC carrying the item */ private void setDefaultCrit(final PlayerCharacter aPC) { if (isWeapon()) { if (aPC != null && EqToken.getOldBonusedCritRange(aPC, this, true) == 0) { getEquipmentHead(1).put(IntegerKey.CRIT_RANGE, 1); } if (getCritMultiplier() == 0) { getEquipmentHead(1).put(IntegerKey.CRIT_MULT, 2); } } } /** * Set the quantity of items * * @param argQty the quantity of items to set */ public void setQty(final double argQty) { qty = argQty; } /** * Clear out the Equipment types */ static void clearEquipmentTypes() { S_EQUIPMENT_TYPES.clear(); } /** * Get the type list as a period-delimited string * * @param bPrimary * if true the types for the porimary head, otherwise the * secondary head. * @return The type value */ String getType(final boolean bPrimary) { final List<String> typeList = typeList(bPrimary); final int typeSize = typeList.size(); final String aType = typeList.stream().collect(Collectors.joining(".")); // Just a // guess. return aType; } boolean save(final BufferedWriter output) { FileAccess.write(output, "BASEITEM:" + formatSaveLine('\t', ':')); FileAccess.newLine(output); return true; } /** * Sets the base attribute of the Equipment object * * Todo remove the pc parameter, it is unused. */ public void setBase() { if (get(ObjectKey.BASE_ITEM) == null) { Equipment eq = Globals.getContext().getReferenceContext() .silentlyGetConstructedCDOMObject(Equipment.class, getKeyName()); if (eq != null) { put(ObjectKey.BASE_ITEM, CDOMDirectSingleRef.getRef(eq)); } else { Logging.errorPrint( "Unable to find base item for " + this.getDisplayName() + " with key " + getKeyName()); } } if (hasConsolidatedProfName()) { CDOMSingleRef<Equipment> baseItem = get(ObjectKey.BASE_ITEM); if (baseItem != null) { Equipment eq = baseItem.get(); CDOMSingleRef<WeaponProf> wpRef = eq.get(ObjectKey.WEAPON_PROF); if (wpRef != null) { put(ObjectKey.WEAPON_PROF, wpRef); } CDOMSingleRef<ArmorProf> apRef = eq.get(ObjectKey.ARMOR_PROF); if (apRef != null) { put(ObjectKey.ARMOR_PROF, apRef); } CDOMSingleRef<ShieldProf> spRef = eq.get(ObjectKey.SHIELD_PROF); if (spRef != null) { put(ObjectKey.SHIELD_PROF, spRef); } } } } public String consolidatedProfName() { if (isWeapon()) { CDOMSingleRef<WeaponProf> wpRef = get(ObjectKey.WEAPON_PROF); if (wpRef != null) { return wpRef.get().getKeyName(); } } else if (isArmor()) { return getArmorProf().getKeyName(); } else if (isShield()) { return getShieldProf().getKeyName(); } return ""; } private boolean hasConsolidatedProfName() { if (isWeapon()) { return get(ObjectKey.WEAPON_PROF) != null; } else if (isArmor()) { return get(ObjectKey.ARMOR_PROF) != null; } else if (isShield()) { return get(ObjectKey.SHIELD_PROF) != null; } return false; } /** * Gets the acceptsTypes attribute of the Equipment object * * @param aType the Type of Equipment that may be contained in this one * * @return the number of aType that may be contained in this Equipement */ private Float getChildType(final String aType) { return d_childTypes.get(aType); } /** * Get the index of a piece of Equipment contained in this one. * * @param e the contained equipment * * @return index of containedEquipment object */ private int getContainedEquipmentIndexOf(final Equipment e) { return d_containedEquipment.indexOf(e); } /** * @param aPC The PC with the equipment * @param saSize The size to adjust for * @return The costAdjustedForSize value */ private BigDecimal getCostAdjustedForSize(final PlayerCharacter aPC, final SizeAdjustment saSize) { BigDecimal c = getSafe(ObjectKey.COST); // // Scale everything to medium before conversion // SizeAdjustment saBase = getSafe(ObjectKey.BASESIZE).get(); if (saSize == null) { return c; } if (aPC != null) { final double saDbl = aPC.getSizeBonusTo(saSize, "ITEMCOST", typeList(), 1.0); final double saBaseDbl = aPC.getSizeBonusTo(saBase, "ITEMCOST", typeList(), 1.0); final double mult = saDbl / saBaseDbl; c = c.multiply(new BigDecimal(mult)); } // // TODO:Non-humanoid races can also double the cost (armor) // return c; } /** * return the list of modifier keys as a period-delimeted string * * @param bPrimary * Description of the Parameter * @return The eqModifierString value */ private String getEqModifierString(final boolean bPrimary) { final List<EquipmentModifier> eqModList = getEqModifierList(bPrimary); final StringBuilder aString = new StringBuilder(eqModList.size() * 10); for (EquipmentModifier eqMod : eqModList) { if (aString.length() != 0) { aString.append('.'); } aString.append(eqMod.getKeyName()); // Add the modifiers for (String strMod : getAssociationList(eqMod)) { aString.append('|').append(strMod.replace('|', '=')); } } if (bPrimary) { BigDecimal mod = get(ObjectKey.WEIGHT_MOD); if (mod != null) { if (aString.length() != 0) { aString.append('.'); } aString.append(EQMOD_WEIGHT).append('|').append(mod.toString().replace('.', ',')); } } String dmg = get(StringKey.DAMAGE_OVERRIDE); if (dmg != null) { if (aString.length() != 0) { aString.append('.'); } aString.append(EQMOD_DAMAGE).append('|').append(dmg.replace('.', ',')); } return aString.toString(); } /** * Set the Type this item will be indexed under * * @param aType the Type this item is indexed under */ private void setIndexedUnderType(final String aType) { indexedUnderType = aType; } /** * Gets the Type this item is indexed under * * @return The Type */ private String isIndexedUnderType() { return indexedUnderType; } /** * Look for a modifier that grants type "magic" * * @param eqModList * Description of the Parameter * @return The magicBonus value */ private static EquipmentModifier getMagicBonus(final Iterable<EquipmentModifier> eqModList) { if (eqModList != null) { for (EquipmentModifier eqMod : eqModList) { if (eqMod.isType("MagicalEnhancement") || (eqMod.isIType("Magic"))) { return eqMod; } } } return null; } /** * Gets the nameFromModifiers attribute of the Equipment object * * @param eqModList * The list of modifiers * @return The nameFromModifiers value */ private String getNameFromModifiers(final List<EquipmentModifier> eqModList) { // // Get a sorted list so that the description will always come // out the same reguardless of the order we've added the modifiers // final List<EquipmentModifier> eqList = new ArrayList<>(eqModList); Globals.sortPObjectList(eqList); final StringBuilder sMod = new StringBuilder(70); eqList.stream().map(eqMod -> eqMod.getSafe(ObjectKey.NAME_OPT).returnName(this, eqMod)).forEach(modDesc -> { if (sMod.length() > 0 && !modDesc.isEmpty()) { sMod.append('/'); } sMod.append(modDesc); }); return sMod.toString(); } /** * Gets the specialAbilityList attribute of the Equipment object * * @param eqModList * Description of the Parameter * @param pc * The PC with the equipment * @return The specialAbilityList value */ private List<String> getSpecialAbilityList(final Iterable<EquipmentModifier> eqModList, final PlayerCharacter pc) { final List<String> saList = new ArrayList<>(); for (EquipmentModifier eqMod : eqModList) { saList.addAll(eqMod.getSpecialProperties(this, pc)); } return saList; } /** * Tack on the cost of the magical enhancement(s). * * @param iPlus the Pluses of the primary head * @param altPlus the Pluses of the secondary head * @return cost from pluses */ private BigDecimal getCostFromPluses(final int iPlus, final int altPlus) { if (((iPlus != 0) || (altPlus != 0)) && (JEPResourceChecker.getMissingResourceCount() == 0)) { PJEP myParser = null; try { myParser = PjepPool.getInstance().aquire(); myParser.addVariable("PLUS", iPlus); myParser.addVariable("ALTPLUS", altPlus); myParser.addVariable("BASECOST", getSafe(ObjectKey.COST).doubleValue()); if (isAmmunition()) { myParser.addVariable("BASEQTY", getSafe(IntegerKey.BASE_QUANTITY)); } String typeMatched; // Look for an expression for all of this item's types // If there is more than 1, use the most expensive. String costExpr; BigDecimal maxCost = null; final List<String> itemTypes = typeList(); for (int idx = 0; idx < itemTypes.size(); ++idx) { typeMatched = itemTypes.get(idx); costExpr = SettingsHandler.getGame().getPlusCalculation(typeMatched); if (costExpr != null) { final BigDecimal thisCost = evaluateCost(myParser, costExpr); if ((maxCost == null) || (thisCost.compareTo(maxCost) > 1)) { maxCost = thisCost; } } } if (maxCost != null) { return maxCost; } // // No cost formula found, check for catch-all definition // typeMatched = "ANY"; costExpr = SettingsHandler.getGame().getPlusCalculation(typeMatched); if (costExpr != null) { return evaluateCost(myParser, costExpr); } } finally { PjepPool.getInstance().release(myParser); } } return BigDecimal.ZERO; } /** * As per p.176 of DMG. * * @return TRUE if limited, else FALSE */ private boolean isMagicLimitedType() { boolean limited = false; if (isType("HEADGEAR") || isType("EYEGEAR") || isType("CAPE") || isType("AMULET") || isSuit() || isType("ROBE") || isType("SHIRT") || isType("BRACER") || isType("GLOVE") || isType("RING") || isType("BELT") || isType("BOOT")) { limited = true; } return limited; } /** * same as getSpecialAbilityList except if you have the same ability * twice, it only lists it once with (2) at the end. * * @param abilityList The list of abilities * @return The specialAbilityTimesList value */ private List<String> getSpecialAbilityTimesList(final List<String> abilityList) { final List<String> sortList = new ArrayList<>(); final int[] numTimes = new int[abilityList.size()]; for (int i = 0; i < abilityList.size(); i++) { final String ability = abilityList.get(i); if (!sortList.contains(ability)) { sortList.add(ability); numTimes[i] = 1; } else { for (int j = 0; j < sortList.size(); j++) { final String testAbility = sortList.get(j); if (testAbility.equals(ability)) { numTimes[j]++; } } } } final List<String> retList = new ArrayList<>(); for (int i = 0; i < sortList.size(); i++) { String ability = sortList.get(i); if (numTimes[i] > 1) { ability = ability + " (" + numTimes[i] + ")"; } retList.add(ability); } return retList; } /** * Gets the weightAdjustedForSize attribute of the Equipment object * * @param aPC the PC with the Equipment * @param newSA the size to adjust for * @return The weightAdjustedForSize value */ private BigDecimal getWeightAdjustedForSize(final PlayerCharacter aPC, final SizeAdjustment newSA) { if (this.isVirtual()) { return BigDecimal.ZERO; } final SizeAdjustment currSA = getSafe(ObjectKey.SIZE).get(); BigDecimal weight = getBaseWeight(); if ((newSA == null) || (currSA == null)) { return weight; } if (aPC != null) { final double mult = aPC.getSizeBonusTo(newSA, "ITEMWEIGHT", typeList(), 1.0) / aPC.getSizeBonusTo(currSA, "ITEMWEIGHT", typeList(), 1.0); weight = weight.multiply(new BigDecimal(mult)); } return weight; } /** * Add a piece of Equipement * * @param e the Equipement to add */ private void addContainedEquipment(final Equipment e) { d_containedEquipment.add(e); } /** * Gets the acModAdjustedForSize attribute of the Equipment object * * @param aPC The PC with the Equipment * @param baseEq The unmodified Equipment * @param newSA The size to adjust for */ private void adjustACForSize(final PlayerCharacter aPC, final Equipment baseEq, final SizeAdjustment newSA) { if ((getRawBonusList(aPC) != null) && isArmor()) { double mult = 1.0; final SizeAdjustment currSA = baseEq.getSafe(ObjectKey.SIZE).get(); if ((newSA != null) && aPC != null) { mult = aPC.getSizeBonusTo(newSA, "ACVALUE", baseEq.typeList(), 1.0) / aPC.getSizeBonusTo(currSA, "ACVALUE", baseEq.typeList(), 1.0); } final List<BonusObj> baseEqBonusList = baseEq.getRawBonusList(aPC); final List<BonusObj> eqBonusList = getRawBonusList(aPC); // // Go through the bonus list looking for COMBAT|AC|x and resize // bonus // Assumption: baseEq.bonusList and this.bonusList only differ in // COMBAT|AC|x bonuses // for (int i = eqBonusList.size() - 1; i >= 0; --i) { final BonusObj aBonus = eqBonusList.get(i); String aString = aBonus.toString(); if (aString.startsWith("COMBAT|AC|")) { final int iOffs = aString.indexOf('|', 10); if (iOffs > 10) { /* * TODO This is bad behavior to alter this list, * which - theoretically - shouldn't be altered * after data load. However, given .REPLACE * potential in BONUS objects, I can't find * another quick solution to this problem * - thpr 10/9/08 */ removeFromListFor(ListKey.BONUS, aBonus); } } } for (final BonusObj aBonus : baseEqBonusList) { String aString = aBonus.toString(); if (aString.startsWith("COMBAT|AC|")) { final int iOffs = aString.indexOf('|', 10); if (iOffs > 10) { Integer acCombatBonus = Integer.valueOf(aString.substring(10, iOffs)); double d = acCombatBonus.doubleValue() * mult; acCombatBonus = (int) d; aString = aString.substring(0, 10) + acCombatBonus.toString() + aString.substring(iOffs); /* * TODO This is bad behavior to alter this list, * which - theoretically - shouldn't be altered * after data load. However, given .REPLACE * potential in BONUS objects, I can't find * another quick solution to this problem * - thpr 10/9/08 */ BonusObj b = Bonus.newBonus(Globals.getContext(), aString); if (b != null) { addToListFor(ListKey.BONUS, b); } } } } } } /** * Test whether the container would be within its weight limits if we * added an item of weight aFloat * * @param aPC * The PC with the Equipment * @param aFloat * The weight of the item we want to add to the container * @return * True if the container is capable of holding the item */ private boolean checkChildWeight(final PlayerCharacter aPC, final Float aFloat) { BigDecimal weightCap = get(ObjectKey.CONTAINER_WEIGHT_CAPACITY); return weightCap != null && (Capacity.UNLIMITED.equals(weightCap) || (aFloat + getContainedWeight(aPC)) <= weightCap.doubleValue()); } /** * Does this item fit in this container * * @param aTypeList * The type list * @param aQuant * The total number of the item * @return Does the item fit */ private boolean checkContainerCapacity(final SortedSet<String> aTypeList, final Float aQuant) { return Capacity.ANY.equals(get(ObjectKey.TOTAL_CAPACITY)) || !("".equals(pickChildType(aTypeList, aQuant))); } private List<EquipmentModifier> cloneEqModList(Equipment other, boolean primary) { final List<EquipmentModifier> clonedList = new ArrayList<>(); for (EquipmentModifier eqMod : getEqModifierList(primary)) { // only make a copy if we need to add qualifiers to modifier if (!eqMod.getSafe(StringKey.CHOICE_STRING).isEmpty()) { EquipmentModifier newEqMod = eqMod.clone(); other.assocSupt.convertAssociations(eqMod, newEqMod); eqMod = newEqMod; } clonedList.add(eqMod); } return clonedList; } /** * Checks whether the child type is possessed * * @param aType the Type to check * * @return true if has child type */ private boolean containsChildType(final String aType) { return d_childTypes.containsKey(aType); } /** * a set which is a sorted collection of the types in the Equipment * * @return a sorted set of the types */ private SortedSet<String> eqTypeList() { return new TreeSet<>(typeList()); } /** * Get all the modifiers that apply to the entire item into a separate list * * @param commonList * The list to extract from * @param extractList * The list to extract. */ private static void extractListFromCommon(final List<EquipmentModifier> commonList, final List<EquipmentModifier> extractList) { for (int i = extractList.size() - 1; i >= 0; --i) { final EquipmentModifier eqMod = extractList.get(i); if (!eqMod.getSafe(ObjectKey.ASSIGN_TO_ALL)) { continue; } commonList.add(0, eqMod); extractList.remove(i); } } private BigDecimal evaluateCost(final PJEP myParser, final String costExpr) { myParser.parseExpression(costExpr); if (!myParser.hasError()) { final Object result = myParser.getValueAsObject(); if (result != null) { return new BigDecimal(result.toString()); } } Logging.errorPrint("Bad equipment cost expression: " + costExpr); return BigDecimal.ZERO; } private boolean ignoresCostDouble() { boolean noDouble = false; if (isType("MANTLE") // Mantle of Spell Resistance doesn't double // cost || isType("POTION") || isType("SCROLL") || isType("STAFF") || isType("WAND")) { noDouble = true; } return noDouble; } /** * Checks whether the Equipment can hold quantToAdd more of an item which * has the types in aTypeList * * @param aTypeList The types of the Equipment we want to add * * @param quantToAdd how many to add * * @return true if the Equipement can hold quantToAdd more of the item */ private String pickChildType(final SortedSet<String> aTypeList, final Float quantToAdd) { Capacity totalCap = get(ObjectKey.TOTAL_CAPACITY); BigDecimal capValue = totalCap == null ? BigDecimal.ZERO : totalCap.getCapacity(); if (getChildType("Total") == null) { setChildType("Total", 0.0f); } String canContain = ""; if ((getChildType("Total") + quantToAdd) <= capValue.doubleValue()) { boolean anyContain = false; float childType = containsChildType("Any") ? getChildType("Any") : 0.0f; CAPFOR: for (Capacity c : getSafeListFor(ListKey.CAPACITY)) { String capType = c.getType(); double val = c.getCapacity().doubleValue(); for (String aType : aTypeList) { if (capType.equalsIgnoreCase(aType)) { if (containsChildType(aType) && ((getChildType(aType) + quantToAdd) <= val) || quantToAdd <= val) { canContain = aType; break CAPFOR; } } else if ("Any".equalsIgnoreCase(capType)) { if ((childType + quantToAdd) <= val) { anyContain = true; } } } } if (("".equals(canContain)) && anyContain) { if (!containsChildType("Any")) { setChildType("Any", (float) 0); } canContain = "Any"; } } return canContain; } /** * Remove the common modifiers from the alternate list. * * @param altList the list of modifiers on the secondary head * @param commonList The list of modifiers common between the two heads * @param errMsg the error message to print if something goes wrong */ private void removeCommonFromList(final List<EquipmentModifier> altList, final List<EquipmentModifier> commonList, final String errMsg) { for (int i = altList.size() - 1; i >= 0; --i) { final EquipmentModifier eqMod = altList.get(i); if (!eqMod.getSafe(ObjectKey.ASSIGN_TO_ALL)) { continue; } final int j = commonList.indexOf(eqMod); if (j >= 0) { altList.remove(i); } else { Logging.errorPrint(errMsg + eqMod.getDisplayName()); } } } /** * Initialise an array of equipment modifier lists with an entry for each * format category. * * @return An array of equipmod lists. */ private List<List<EquipmentModifier>> initSplitModList() { List<List<EquipmentModifier>> modListArray = IntStream.range(0, EqModFormatCat.values().length) .<List<EquipmentModifier>>mapToObj(i -> new ArrayList<>()).collect(Collectors.toList()); return modListArray; } /** * Split the equipmod list into seperate lists by format category. * * @param modList * The list to be split. * @param splitModList * The array of receiving lists, one for each format cat. */ private void splitModListByFormatCat(final List<EquipmentModifier> modList, final List<List<EquipmentModifier>> splitModList) { for (EquipmentModifier aModList : modList) { int o = aModList.getSafe(ObjectKey.FORMAT).ordinal(); splitModList.get(o).add(aModList); } } /** * remove contained Equipment * * @param i the index of the item to remove */ private void removeContainedEquipment(final int i) { d_containedEquipment.remove(i); } /** * Remove an equipment modifier and specified associated information eg. * Bane|Vermin|Fey eg. Keen Removes a feature from the EqModifier attribute * of the Equipment object * * @param aString * The feature to be removed from the EqModifier attribute * @param bPrimary * The feature to be removed from the EqModifier attribute * @param aPC * the PC that has the Equipment */ private void removeEqModifier(final String aString, final boolean bPrimary, PlayerCharacter aPC) { final StringTokenizer aTok = new StringTokenizer(aString, "|"); final String eqModKey = aTok.nextToken(); final EquipmentModifier eqMod = getEqModifierKeyed(eqModKey, bPrimary); if (eqMod == null) { return; } // // Remove the associated choices // while (aTok.hasMoreTokens()) { final String x = aTok.nextToken().replace('=', '|'); getAssociationList(eqMod).stream().filter(aChoice -> aChoice.startsWith(x)) .forEach(aChoice -> removeAssociation(eqMod, aChoice)); } if (!hasAssociations(eqMod)) { removeEqModifier(eqMod, bPrimary, aPC); } } /** * Returns a list of the types of this item. * * @param bPrimary * if true return the types if the primary head, otherwise * return the types of the secondary head * @return a list of the types of this item. */ private List<String> typeList(final boolean bPrimary) { if (bPrimary && usePrimaryCache) { return typeListCachePrimary; } if (!bPrimary && useSecondaryCache) { return typeListCacheSecondary; } // Use the primary type(s) if none defined for secondary List<Type> initializingList = getEquipmentHead(2).getListFor(ListKey.TYPE); if (bPrimary || (initializingList == null) || initializingList.isEmpty()) { initializingList = getTrueTypeList(false); } else if (!isDouble()) { return new ArrayList<>(); } Set<String> calculatedTypeList = new LinkedHashSet<>(); if (initializingList != null) { for (Type t : initializingList) { calculatedTypeList.add(t.getComparisonString()); } } final Collection<String> modTypeList = new ArrayList<>(); // // Add in all type modfiers from "ADDTYPE" modifier // EquipmentModifier aEqMod = getEqModifierKeyed("ADDTYPE", bPrimary); if (aEqMod != null) { for (String aType : getAssociationList(aEqMod)) { aType = aType.toUpperCase(); if (!calculatedTypeList.contains(aType)) { modTypeList.add(aType); } } } /* * CONSIDER I think there is a weird order of operations issue nere, need to check * if it existed way back, e.g. SVN 6206. The issue is if a Type is introduced by a * MOD, then the ChangeArmorType system doesn't seem to be able to grab/modify it * Is that correct? - thpr 10/3/08 */ // // Add in all of the types from each EquipmentModifier // currently applied to this piece of equipment // final List<EquipmentModifier> eqModList = getEqModifierList(bPrimary); for (EquipmentModifier eqMod : eqModList) { // // If we've just replaced the armor type, then make sure it is // not in the equipment modifier list // Set<String> newTypeList = new LinkedHashSet<>(calculatedTypeList); for (ChangeArmorType cat : eqMod.getSafeListFor(ListKey.ARMORTYPE)) { List<String> tempTypeList = cat.applyProcessor(newTypeList); LinkedHashSet<String> tempTypeSet = new LinkedHashSet<>(tempTypeList); boolean noMatch = newTypeList.size() != tempTypeList.size() || newTypeList.equals(tempTypeSet); newTypeList = tempTypeSet; if (!noMatch) { break; } } Collection<String> removedTypeList = new ArrayList<>(calculatedTypeList); removedTypeList.removeAll(newTypeList); modTypeList.removeAll(removedTypeList); calculatedTypeList = newTypeList; for (String aType : eqMod.getSafeListFor(ListKey.ITEM_TYPES)) { aType = aType.toUpperCase(); // If it's BOTH & MELEE, we cannot add RANGED or THROWN to // it // BOTH is only used after the split of a Thrown weapon in 2 // (melee and ranged) if (calculatedTypeList.contains("BOTH") && calculatedTypeList.contains("MELEE") && ("RANGED".equals(aType) || "THROWN".equals(aType))) { continue; } if (!calculatedTypeList.contains(aType) && !modTypeList.contains(aType)) { modTypeList.add(aType); } } } calculatedTypeList.addAll(modTypeList); // // Make sure MAGIC tag is the 1st entry // List<String> resultingTypeList = new ArrayList<>(calculatedTypeList); final int idx = resultingTypeList.indexOf("MAGIC"); if (idx > 0) { resultingTypeList.remove(idx); resultingTypeList.add(0, "MAGIC"); } if (bPrimary) { typeListCachePrimary = resultingTypeList; usePrimaryCache = true; } else { typeListCacheSecondary = resultingTypeList; useSecondaryCache = true; } return resultingTypeList; } /** * Creates the containerCapacityString from children of this object */ private void updateContainerCapacityString() { final StringBuilder tempStringBuilder = new StringBuilder(100); boolean comma = false; BigDecimal weightCap = get(ObjectKey.CONTAINER_WEIGHT_CAPACITY); if (weightCap != null && !Capacity.UNLIMITED.equals(weightCap)) { tempStringBuilder.append(weightCap).append(' ').append(Globals.getGameModeUnitSet().getWeightUnit()); comma = true; } List<Capacity> capacity = getListFor(ListKey.CAPACITY); if (capacity != null) { for (Capacity c : capacity) { if (comma) { tempStringBuilder.append(", "); comma = false; } BigDecimal capValue = c.getCapacity(); if (!Capacity.UNLIMITED.equals(capValue)) { tempStringBuilder.append(capValue).append(' '); tempStringBuilder.append(c.getType()); comma = true; } else if (c.getType() != null) { comma = true; tempStringBuilder.append(c.getType()); } } } containerCapacityString = tempStringBuilder.toString(); } /** * Sets all the BonusObj's to "active". Note this version overrides the * PObject implementation as it will check the bonuses against the * equipment, rather than the PC. * * @param aPC * The character being checked. */ @Override public void activateBonuses(final PlayerCharacter aPC) { for (final BonusObj bonus : getRawBonusList(aPC)) { aPC.setApplied(bonus, PrereqHandler.passesAll(bonus, this, aPC)); } } public boolean isCalculatingCost() { return calculatingCost; } public boolean isWeightAlreadyUsed() { return weightAlreadyUsed; } public BigDecimal getWeightInPounds() { return isVirtual() ? BigDecimal.ZERO : getSafe(ObjectKey.WEIGHT); } public void setWeightAlreadyUsed(boolean weightAlreadyUsed) { this.weightAlreadyUsed = weightAlreadyUsed; } /** * Get non headed name * * @return non headed name */ public String getNonHeadedName() { if (wholeItemName == null || wholeItemName.isEmpty()) { return getName(); } return wholeItemName; } /** * Get whole item name * * @return whole item name */ public String getWholeItemName() { return wholeItemName; } /** * Set whole item name * * @param wholeItemName the name to set */ void setWholeItemName(String wholeItemName) { this.wholeItemName = wholeItemName; } /** * Create a Key for the new custom piece of resized equipment. The new key * will start with the auto resized constant and then the size abbreviation * (as per SizeAdjustment) followed by the existing key. This should * generate a unique name unless we've already auto resized this piece of * equipment to this size in which case it already exists in the equipment * list and does not need to be created. * * @param newSize * The size of equipment to make a key for. This needs to be the * abbreviated form, not the full name. * @return The generated key */ public String createKeyForAutoResize(SizeAdjustment newSize) { // Make sure newSize is not null if (newSize == null) { return getKeyName(); } String displayName = newSize.getDisplayName(); // Make sure finalSize is a single upper case letter String finalSize = displayName.toUpperCase().substring(0, 1); String thisKey = getKeyName(); if (thisKey.startsWith(Constants.AUTO_RESIZE_PREFIX)) { int index = Constants.AUTO_RESIZE_PREFIX.length(); String keySize = thisKey.substring(index, index + 1).toUpperCase(); // If the key of this object already has the finalSize in the correct // place then just return it, the item has already been adjusted. // This should never happen because if the key has an AUTO_RESIZE_PREFIX // prefix and the correct size then it should already be finalSize if (keySize.equals(finalSize)) { return thisKey; } // remove the AUTO_RESIZE_PREFIX and the following size abbreviation // from the key thisKey = thisKey.substring(index + 1); } return Constants.AUTO_RESIZE_PREFIX + finalSize + thisKey; } /** * Create a Name for the new custom piece of resized equipment. The name * will be constructed by searching for the size of the equipment in its * name. If found (and surrounded by '(', '/', or ')', it will be replaced. * If not found, it will be added to the end surrounded by parenthesis. * * @param newSize * The size of equipment to make a key for * @return The generated Name */ public String createNameForAutoResize(SizeAdjustment newSize) { // Make sure newSize is not null if (newSize == null) { return getName(); } String displayName = newSize.getDisplayName(); String thisName = getName(); String upName = thisName.toUpperCase(); // Get the full name of the current size SizeAdjustment sa1 = getSafe(ObjectKey.SIZE).get(); String upThisSize = sa1.getDisplayName().toUpperCase(); int start = upName.indexOf(upThisSize); int end = start + upThisSize.length(); /* * if the name contains thisSize surrounded by /, ( or ) then replace * thisSize with newSize */ if (start > -1 && (upName.substring(start - 1).startsWith("(") || upName.substring(start - 1).startsWith("/")) && (upName.substring(end).startsWith(")") || upName.substring(end).startsWith("/"))) { return thisName.substring(0, start) + displayName + thisName.substring(end); } return thisName + " (" + displayName + ")"; } /** * Make this item virtual i.e. one that doesn't really exist and is only * used to hold temporary bonuses */ public void makeVirtual() { this.virtualItem = true; } /** * Does this item really exist, or is it a phantom created to hold a bonus * * @return Returns the virtualItem. */ private boolean isVirtual() { return virtualItem; } /** * Gets the critMultiplier attribute of the Equipment object * * @return The critMultiplier value * @deprecated due to CRITMULT code control */ @Deprecated public int getCritMultiplier() { int mult = getHeadInfo(1, IntegerKey.CRIT_MULT); if (mult == 0) { final String cm = getWeaponInfo("CRITMULT", true); if (!cm.isEmpty()) { mult = Integer.parseInt(cm); } } return mult; } /** * Gets the altCritMultiplier attribute of the Equipment object * * @return The altCritMultiplier value * @deprecated due to CRITMULT code control */ @Deprecated public int getAltCritMultiplier() { int mult = getHeadInfo(2, IntegerKey.CRIT_MULT); if (mult == 0) { final String cm = getWeaponInfo("CRITMULT", false); if (!cm.isEmpty()) { mult = Integer.parseInt(cm); } } return mult; } /** * @deprecated due to CRITMULT and CRITRANGE code controls */ @Deprecated private int getHeadInfo(int headnum, IntegerKey ik) { EquipmentHead head = getEquipmentHeadReference(headnum); return head == null ? 0 : head.getSafe(ik); } /** * Test to see if a weapon is Finesseable or not for a PC * * @param pc The PlayerCharacter wielding the weapon. * @return true if finessable */ public boolean isFinessable(final PlayerCharacter pc) { WieldCategory wCat = getEffectiveWieldCategory(pc); return isType("Finesseable") || (wCat != null && wCat.isFinessable()); } /** * Tests if this weapon is a light weapon for the specied PC. * * @param pc The PlayerCharacter wielding the weapon. * @return true if the weapon is light for the specified pc. */ public boolean isWeaponLightForPC(final PlayerCharacter pc) { if (pc == null || !isWeapon()) { return false; } WieldCategory wc = Globals.getContext().getReferenceContext() .silentlyGetConstructedCDOMObject(WieldCategory.class, "Light"); return (wc != null) && wc.equals(getEffectiveWieldCategory(pc)); } /** * Tests if this weapon can be used in one hand by the specified PC. * * @param pc The PlayerCharacter wielding the weapon. * @return true if the weapon can be used one handed. */ public boolean isWeaponOneHanded(final PlayerCharacter pc) { if (pc == null && !isWeapon()) { return false; } WieldCategory wCat = getEffectiveWieldCategory(pc); return wCat != null && wCat.getHandsRequired() == 1; } /** * Tests if the weapon is either too large OR too small for the specified PC * to wield. * * @param pc The PlayerCharacter wielding the weapon. * @return true if the weapon is too large or too small. */ public boolean isWeaponOutsizedForPC(final PlayerCharacter pc) { if (pc == null || !isWeapon()) { return true; } final WieldCategory wCat = getEffectiveWieldCategory(pc); return wCat != null && (wCat.getHandsRequired() > 2 || wCat.getHandsRequired() < 0); } /** * Tests is the weapon is too large for the PC to use. * * @param pc The PlayerCharacter wielding the weapon * @return true if the weapon is too large. */ public boolean isWeaponTooLargeForPC(final PlayerCharacter pc) { if (pc == null || !isWeapon()) { return false; } WieldCategory wieldCategory = getEffectiveWieldCategory(pc); return wieldCategory != null && wieldCategory.getHandsRequired() > 2; } /** * Tests if this weapon requires two hands to use. * * @param pc - * The PlayerCharacter wielding the weapon. * @return true if the weapon is two-handed for the specified pc */ public boolean isWeaponTwoHanded(final PlayerCharacter pc) { if (pc == null || !isWeapon()) { return false; } WieldCategory wieldCategory = getEffectiveWieldCategory(pc); return wieldCategory != null && wieldCategory.getHandsRequired() == 2; } /** * Gets the minimum WieldCategory this weapon can be used at. Accounts for * all modifiers that affect WieldCategory. 3.0 weapon sizes are mapped to * appropriate WieldCategories. * * @param aPC The PlayerCharacter using the weapon * @return The minimum WieldCategory required to use the weapon. */ public WieldCategory getEffectiveWieldCategory(final PlayerCharacter aPC) { CDOMSingleRef<WeaponProf> ref = get(ObjectKey.WEAPON_PROF); WeaponProf wp = ref == null ? null : ref.get(); WieldCategory wCat = get(ObjectKey.WIELD); if (wCat != null && !Globals.checkRule(RuleConstants.SIZEOBJ)) { // Get the starting effective wield category wCat = wCat.adjustForSize(aPC, this); } else { int pcSize = aPC.sizeInt(); if (wp != null) { pcSize += aPC.getTotalBonusTo("WEAPONPROF=" + wp.getKeyName(), "PCSIZE"); } int sizeDiff; if (wCat != null && Globals.checkRule(RuleConstants.SIZEOBJ)) { // In this case we have a 3.5 style equipments size. // We need to map to a 3.0 style sizeDiff = wCat.getObjectSizeInt(this) - pcSize; } else { sizeDiff = sizeInt() - pcSize; } if (sizeDiff > 1) { wCat = Globals.getContext().getReferenceContext() .silentlyGetConstructedCDOMObject(WieldCategory.class, "TooLarge"); } else if (sizeDiff == 1) { wCat = Globals.getContext().getReferenceContext() .silentlyGetConstructedCDOMObject(WieldCategory.class, "TwoHanded"); } else if (sizeDiff == 0) { wCat = Globals.getContext().getReferenceContext() .silentlyGetConstructedCDOMObject(WieldCategory.class, "OneHanded"); } else { wCat = Globals.getContext().getReferenceContext() .silentlyGetConstructedCDOMObject(WieldCategory.class, "Light"); } } int aBump = 0; // TODO Remove this code when support for this "feature" goes away if (wp != null) { int iHands = wp.getSafe(IntegerKey.HANDS); if (iHands == Constants.HANDS_SIZE_DEPENDENT) { if (aPC.sizeInt() > sizeInt()) { iHands = 1; } else { iHands = 2; } } while (wCat.getHandsRequired() < iHands) { wCat = wCat.getWieldCategoryStep(1); } // See if there is a bonus associated with just this weapon final String expProfName = wp.getKeyName(); aBump += (int) aPC.getTotalBonusTo("WEAPONPROF=" + expProfName, "WIELDCATEGORY"); // loops for each equipment type int modWield = 0; for (String eqType : typeList()) { final StringBuilder sB = new StringBuilder("WEAPONPROF=TYPE."); sB.append(eqType); // get the type bonus (ex TYPE.MARTIAL) final int i = (int) aPC.getTotalBonusTo(sB.toString(), "WIELDCATEGORY"); // get the highest bonus if (i < modWield) { modWield = i; } } aBump += modWield; } // or a bonus from the weapon itself aBump += (int) bonusTo(aPC, "WEAPON", "WIELDCATEGORY", true); if (aBump == 0) { return wCat; } return wCat.getWieldCategoryStep(aBump); } // // Protective Item Support // /** * Gets the acMod attribute of the Equipment object * * @param aPC The PC that has the Equipment * * @return The acMod value */ public Integer getACMod(final PlayerCharacter aPC) { String acMod = aPC.getControl(CControl.EQACMOD); if (acMod != null) { Object o = aPC.getLocal(this, acMod); return ((Number) o).intValue(); } //TODO This should be documented return (int) bonusTo(aPC, "EQMARMOR", "AC", true) + (int) bonusTo(aPC, "COMBAT", "AC", true); } // // Weapon Support // /** * Gets the damage attribute of the Equipment object * * @param aPC The PC that has the Equipment * * @return The damage value */ public String getDamage(final PlayerCharacter aPC) { return getDamage(aPC, true); } private String getDamage(PlayerCharacter apc, boolean bPrimary) { int headnum = bPrimary ? 1 : 2; EquipmentHead head = getEquipmentHeadReference(headnum); if (head == null) { return ""; } String dam = head.get(StringKey.DAMAGE); if (!isWeapon() || (!bPrimary && !isDouble())) { return dam == null ? "" : dam; } if (bPrimary && dam == null) { // No need to grab reference, always exists due to if above EquipmentHead altHead = getEquipmentHead(2); dam = altHead.get(StringKey.DAMAGE); } String override = get(StringKey.DAMAGE_OVERRIDE); if (bPrimary && override != null) { // this overides the base damage dam = override; } if (dam == null) { dam = getWeaponInfo("DAMAGE", bPrimary); } final int iSize = sizeInt(); int iMod = iSize + (int) bonusTo(apc, "EQMWEAPON", "DAMAGESIZE", bPrimary); iMod += (int) bonusTo(apc, "WEAPON", "DAMAGESIZE", bPrimary); AbstractReferenceContext ref = Globals.getContext().getReferenceContext(); if (iMod < 0) { iMod = 0; } else { int maxIndex = ref.getConstructedObjectCount(SizeAdjustment.class) - 1; if (iMod > maxIndex) { iMod = maxIndex; } } SizeAdjustment sa = ref.getSortedList(SizeAdjustment.class, IntegerKey.SIZEORDER).get(iMod); return adjustDamage(dam, sa); } /** * Returns the alternate damage for this item. * * @param aPC The PC that has the Equipment * * @return the alternate damage for this item. */ public String getAltDamage(final PlayerCharacter aPC) { return getDamage(aPC, false); } /** * Gets the bonusToDamage attribute of the Equipment object * * @param aPC The PC that has the Equipment * * @param bPrimary * if true get info about the priomary head, else get info * about the secondary head. * @return The bonusToDamage value */ public int getBonusToDamage(final PlayerCharacter aPC, final boolean bPrimary) { return (int) bonusTo(aPC, "WEAPON", "DAMAGE", bPrimary); } /** * Gets the bonusToHit attribute of the Equipment object * * @param aPC The PC that has the Equipment * * @param bPrimary * if true get info about the priomary head, else get info * about the secondary head. * @return The bonusToHit value */ public int getBonusToHit(final PlayerCharacter aPC, final boolean bPrimary) { return (int) bonusTo(aPC, "WEAPON", "TOHIT", bPrimary); } // --------------------------- // Owned Equipment // --------------------------- /** * Sets the number of items of this type that are carried. * * @param argCarried * the number of items of this type that are carried. */ public void setCarried(final Float argCarried) { carried = argCarried; } /** * Returns the number of items of this type that are carried. * * @return the number of items of this type that are carried. */ public Float getCarried() { return carried; } /** * Gets the equipped attribute of the Equipment object * * @return The equipped value */ public boolean isEquipped() { return equipped; } // --------------------------- // Container Support // --------------------------- /** * Gets a child of the Equipment object * * @param childIndex * The index of the child to get * @return The child value */ private Object getChild(final int childIndex) { return getContainedEquipment(childIndex); } /** * Gets the childCount attribute of the Equipment object * * @return The childCount value */ public int getChildCount() { return getContainedEquipmentCount(); } /** * Sets how many of a child the Equipment currently holds * * @param aType * A type of Equiupment that may be contained in this one * @param quantity * How many of the Child Type are currently contained */ private void setChildType(final String aType, final Float quantity) { d_childTypes.put(aType, quantity); } /** * @param index * integer indicating which object (contained in this object) to * return * @return the equipment object contained at this position. */ public Equipment getContainedByIndex(final int index) { final List<Equipment> contents = new ArrayList<>(getContents()); if (!contents.isEmpty()) { if (index <= contents.size()) { return contents.get(index); } } return null; } /** * Get a piece of contained equipment * * @param i the index of the contained equipment * * @return containedEquipment object */ public Equipment getContainedEquipment(final int i) { return d_containedEquipment.get(i); } /** * count * * @return number of containedEquipment objects */ public int getContainedEquipmentCount() { return d_containedEquipment.size(); } /** * calculates the value of all items in this container If this container * contains containers, also add the value of all items within that * container, etc, etc, etc. * * @param aPC The PC that has the Equipment * @return contained value */ private double getContainedValue(final PlayerCharacter aPC) { double total = 0; if (getChildCount() == 0) { return total; } for (int e = 0; e < getContainedEquipmentCount(); ++e) { final Equipment anEquip = getContainedEquipment(e); if (anEquip.getContainedEquipmentCount() > 0) { total += anEquip.getContainedValue(aPC); } else { total += anEquip.getCost(aPC).floatValue(); } } return total; } /** * Gets the contained Weight this object recursis all child objects to get * their contained weight * * @param aPC The PC that has the Equipment * * @return The containedWeight value */ public Float getContainedWeight(final PlayerCharacter aPC) { return getContainedWeight(aPC, false); } /** * Get Base contained weight * * @return base contained weight */ private Float getBaseContainedWeight() { return getBaseContainedWeight(false); } /** * Get Base contained weight * * @param effective Should we recurse child objects? * @return Base contained weight */ private Float getBaseContainedWeight(final boolean effective) { Float total = (float) 0; if ((getSafe(ObjectKey.CONTAINER_CONSTANT_WEIGHT) && !effective) || (getChildCount() == 0)) { return total; } for (int e = 0; e < getContainedEquipmentCount(); ++e) { final Equipment anEquip = getContainedEquipment(e); if (anEquip.getContainedEquipmentCount() > 0) { total = total + anEquip.getBaseWeight().floatValue() + anEquip.getBaseContainedWeight(); } else { total += anEquip.getBaseWeight().floatValue() * anEquip.getCarried(); } } Integer crw = get(IntegerKey.CONTAINER_REDUCE_WEIGHT); if (crw != null && crw != 0) { total *= (crw.floatValue() / 100); } return total; } /** * Gets the contained Weight this object recursis all child objects to get * their contained weight * * @param aPC The PC that has the Equipment * * @param effective * Should we recurse child objects? * @return The containedWeight value */ public Float getContainedWeight(final PlayerCharacter aPC, final boolean effective) { Float total = 0.0f; if ((getSafe(ObjectKey.CONTAINER_CONSTANT_WEIGHT) && !effective) || (getChildCount() == 0)) { return total; } for (int e = 0; e < getContainedEquipmentCount(); ++e) { final Equipment anEquip = getContainedEquipment(e); if (anEquip.getContainedEquipmentCount() > 0) { total = new Float(total + anEquip.getWeightAsDouble(aPC) + anEquip.getContainedWeight(aPC)); } else { total = new Float(total + (anEquip.getWeightAsDouble(aPC) * anEquip.getQty())); } } Integer crw = get(IntegerKey.CONTAINER_REDUCE_WEIGHT); if (crw != null && crw != 0) { total *= (crw.floatValue() / 100); } return total; } /** * @param aType * Type and sequencer (e.g. Liquid3) * @param aSubTag * SubTag (NAME or SPROP) * @return a String containing the specified subtag */ public String getContainerByType(String aType, final String aSubTag) { final List<Equipment> contents = new ArrayList<>(getContents()); // Separate the Type from the sequencer (Liquid from 3) int numCharToRemove = 0; for (int i = aType.length() - 1; i > 0; i--) { if ((aType.charAt(i) >= '0') && (aType.charAt(i) <= '9')) { numCharToRemove++; } else { break; } } int typeIndex; String type; if (numCharToRemove > 0) { int l = aType.length() - numCharToRemove; type = aType.substring(0, l); typeIndex = Integer.parseInt(aType.substring(l)); } else { type = aType; typeIndex = -1; } contents.stream().filter(eq -> !eq.isType(type)).forEach(contents::remove); if (typeIndex < contents.size()) { if ("SPROP".equals(aSubTag)) { return contents.get(typeIndex).getRawSpecialProperties(); } return contents.get(typeIndex).getName(); } return " "; } /** * Gets the containerContentsString attribute of the Equipment object * * @return The containerContentsString value */ public String getContainerContentsString() { if (containerContentsString == null) { updateContainerContentsString(null); } return containerContentsString; } /** * Convenience method. <p> <br> * author: Thomas Behr 27-03-02 * * @return a list with all Equipment objects this container holds; if this * instance is no container, the list will be empty. */ public Collection<Equipment> getContents() { final Collection<Equipment> contents = IntStream.range(0, getContainedEquipmentCount()) .mapToObj(this::getContainedEquipment).collect(Collectors.toList()); return contents; } // --------------------------- // Container Definition methods // --------------------------- /** * Gets the containerCapacityString attribute of the Equipment object * * @return The containerCapacityString value */ public String getContainerCapacityString() { if (containerCapacityString == null) { updateContainerCapacityString(); } return containerCapacityString; } /** * Convenience method. <p> <br> * author: Thomas Behr 27-03-02 * * @return <code>true</code>, if this instance is a container; * <code>false</code>, otherwise */ public boolean isContainer() { return acceptsChildren(); } private List<EquipmentHead> heads = new ArrayList<>(); public EquipmentHead getEquipmentHead(int index) { if (index <= 0) { throw new IndexOutOfBoundsException(Integer.toString(index)); } int headsIndex = index - 1; int currentSize = heads.size(); EquipmentHead head; if (headsIndex >= currentSize) { for (int i = 0; i < headsIndex - currentSize; i++) { heads.add(null); } head = new EquipmentHead(this, index); heads.add(head); } else { head = heads.get(headsIndex); if (head == null) { head = new EquipmentHead(this, index); heads.set(headsIndex, head); } } return head; } public EquipmentHead getEquipmentHeadReference(int index) { if (index <= 0) { throw new IndexOutOfBoundsException(Integer.toString(index)); } else if (index <= heads.size()) { return heads.get(index - 1); } return null; } public List<EquipmentHead> getEquipmentHeads() { return new ArrayList<>(heads); } /** * Reduce/increase damage for modified size as per DMG p.162 * * @param aDamage The base damage * @param aSize The size to adjust for * @return The adjusted damage */ private String adjustDamage(final String aDamage, final SizeAdjustment aSize) { if (aDamage == null) { return null; } if (!"special".equalsIgnoreCase(aDamage) && !"-".equals(aDamage)) { return Globals.adjustDamage(aDamage, getSafe(ObjectKey.SIZE).get(), aSize); } return aDamage; } /** * Gets the damageAdjustedForSize attribute of the Equipment object * * @param aSize * The size to adjust for * @param bPrimary * If true get the damage for the primary head, otherwise * get the damage for the secondary head * @return The damageAdjustedForSize value */ private String getDamageAdjustedForSize(final SizeAdjustment aSize, final boolean bPrimary) { int headnum = bPrimary ? 1 : 2; EquipmentHead head = getEquipmentHeadReference(headnum); if (head == null) { return null; } String dam = head.get(StringKey.DAMAGE); if (!isWeapon() || (!bPrimary && !isDouble())) { return dam; } if (dam == null) { dam = getWeaponInfo("DAMAGE", bPrimary); } return adjustDamage(dam, aSize); } public String getWeaponInfo(final String infoType, final boolean bPrimary) { final String it = infoType + "|"; final EquipmentModifier eqMod = getEqModifierKeyed(Constants.INTERNAL_EQMOD_WEAPON, bPrimary); if (eqMod != null) { return getAssociationList(eqMod).stream().filter(aString -> aString.startsWith(it)).findFirst() .map(aString -> aString.substring(it.length())).orElse(""); } return ""; } public ShieldProf getShieldProf() { if (isShield()) { CDOMSingleRef<ShieldProf> ref = get(ObjectKey.SHIELD_PROF); if (ref == null) { ShieldProf sp = Globals.getContext().getReferenceContext() .silentlyGetConstructedCDOMObject(ShieldProf.class, getKeyName()); if (sp == null) { return Globals.getContext().getReferenceContext().constructCDOMObject(ShieldProf.class, getKeyName()); } else { return sp; } } else { return ref.get(); } } return null; } public ArmorProf getArmorProf() { if (isArmor()) { CDOMSingleRef<ArmorProf> ref = get(ObjectKey.ARMOR_PROF); if (ref == null) { ArmorProf ap = Globals.getContext().getReferenceContext() .silentlyGetConstructedCDOMObject(ArmorProf.class, getKeyName()); if (ap == null) { return Globals.getContext().getReferenceContext().constructCDOMObject(ArmorProf.class, getKeyName()); } else { return ap; } } else { return ref.get(); } } return null; } public void addAssociation(CDOMObject obj, String o) { assocSupt.addAssoc(obj, AssociationListKey.CHOICES, new FixedStringList(o)); } public boolean containsAssociated(CDOMObject obj, String o) { List<FixedStringList> list = assocSupt.getAssocList(obj, AssociationListKey.CHOICES); if (list != null) { return list.stream().anyMatch( fsl -> FixedStringList.CASE_INSENSITIVE_ORDER.compare(fsl, new FixedStringList(o)) == 0); } return false; } private int getSelectCorrectedAssociationCount(CDOMObject obj) { Formula f = obj.getSafe(FormulaKey.SELECT); //TODO Null here is probably a problem for the PC :/ int select = f.resolve(this, true, null, "").intValue(); return assocSupt.getAssocCount(obj, AssociationListKey.CHOICES) / select; } public List<String> getAssociationList(CDOMObject obj) { List<String> list = new ArrayList<>(); List<FixedStringList> assocList = assocSupt.getAssocList(obj, AssociationListKey.CHOICES); if (assocList != null) { assocList.stream().map(ac -> ac.get(0)).forEach(choiceStr -> { if (Constants.EMPTY_STRING.equals(choiceStr)) { list.add(null); } else { list.add(choiceStr); } }); } return list; } public boolean hasAssociations(Object obj) { return assocSupt.hasAssocs(obj, AssociationListKey.CHOICES); } public List<String> removeAllAssociations(CDOMObject obj) { List<String> list = getAssociationList(obj); assocSupt.removeAllAssocs(obj, AssociationListKey.CHOICES); return list; } private void removeAssociation(CDOMObject obj, String o) { assocSupt.removeAssoc(obj, AssociationListKey.CHOICES, new FixedStringList(o)); } public String getFirstAssociation(CDOMObject obj) { return assocSupt.getAssocList(obj, AssociationListKey.CHOICES).get(0).get(0); } /** * Get the map of bonuses for this object * @return bonusMap */ public Map<String, String> getBonusMap() { if (bonusMap == null) { bonusMap = new HashMap<>(); } return bonusMap; } /** * Put the key/value pair into the bonus map * * @param aKey The Key to store the bonus under * @param aVal The value of the Bonus */ private void putBonusMap(final String aKey, final String aVal) { getBonusMap().put(aKey, aVal); } /** * @param bonus a Number (such as 2) * @param aType "COMBAT.AC.Dodge" or "COMBAT.AC.Dodge.STACK" */ public void setBonusStackFor(final double bonus, String aType) { String bType = (aType != null) ? aType.toUpperCase() : null; // Default to non-stacking bonuses int index = -1; // e.g. "COMBAT.AC.DODGE" if (aType != null) { final StringTokenizer aTok = new StringTokenizer(bType, "."); if (aTok.countTokens() >= 2) { // we need to get the 3rd token to see // if it should .STACK or .REPLACE aTok.nextToken(); // Discard token String aString = aTok.nextToken(); String nextTok = null; // if the 3rd token is "BASE" we have something like // CHECKS.BASE.Fortitude if ("BASE".equals(aString)) { if (aTok.hasMoreTokens()) { // discard next token (Fortitude) aTok.nextToken(); } if (aTok.hasMoreTokens()) { // check for a TYPE nextTok = aTok.nextToken(); } } else { if (aTok.hasMoreTokens()) { // Type: .DODGE nextTok = aTok.nextToken(); } } if (nextTok != null) { index = SettingsHandler.getGame().getUnmodifiableBonusStackList().indexOf(nextTok); // e.g. // Dodge } // un-named (or un-TYPE'd) bonus should stack if ((nextTok == null) || "NULL".equals(nextTok)) { index = 1; } } } // .STACK means stack // .REPLACE stacks with other .REPLACE bonuses if ((bType != null) && (bType.endsWith(".STACK") || bType.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 = getBonusMap().get(bType); if (aVal == null) { putBonusMap(bType, String.valueOf(bonus)); } else { putBonusMap(bType, String.valueOf(Math.max(bonus, Float.parseFloat(aVal)))); } } else // a stacking bonus { String type = bType == null ? "" : bType.endsWith(".REPLACE.STACK") ? bType.substring(0, bType.length() - 6) : bType; final String aVal = getBonusMap().get(type); if (aVal == null) { putBonusMap(type, String.valueOf(bonus)); } else { putBonusMap(type, String.valueOf(bonus + Float.parseFloat(aVal))); } } } private void dumpTypeCache() { usePrimaryCache = false; useSecondaryCache = false; } public void addType(Type newType) { addToListFor(ListKey.TYPE, newType); dumpTypeCache(); } public void removeType(Type t) { while (removeFromListFor(ListKey.TYPE, t)) { ; // Using the while for side effects } dumpTypeCache(); } /** * Add a Weapon to an Equipment Location. * @param num how many pieces to add * @param eLoc the Location to add the weapon to * @param aPC the PC to quip the weapon on */ public void addWeaponToLocation(Float num, EquipmentLocation eLoc, PlayerCharacter aPC) { Float numEquipped = (eLoc == EquipmentLocation.EQUIPPED_TWO_HANDS) ? 2.0f : num; setNumberEquipped(numEquipped.intValue()); setLocation(eLoc); if (eLoc != EquipmentLocation.EQUIPPED_NEITHER) { setQty(num); setNumberCarried(num); setIsEquipped(true, aPC); } } /** * Add a piece of general equipment to an Equipment Location. * @param num how many pieces to add * @param eLoc the Location to add the equipment to * @param equip whether to equip the item * @param aPC the PC to quip the weapon on */ public void addEquipmentToLocation(Float num, EquipmentLocation eLoc, boolean equip, PlayerCharacter aPC) { setLocation(eLoc); setQty(num); setIsEquipped(equip, aPC); Float numCarried = (eLoc == EquipmentLocation.NOT_CARRIED) ? 0.0f : num; setNumberCarried(numCarried); } /** * The Class <code>EquipmentHeadCostSummary</code> carries the multi * valued response back when calculating the cost of a head. */ private static class EquipmentHeadCostSummary { private BigDecimal postSizeCost = BigDecimal.ZERO; private BigDecimal nonDoubleCost = BigDecimal.ZERO; private int headPlus = 0; } /** * Get the list of temporary bonuses for this list * @return the list of temporary bonuses for this list */ private List<BonusObj> getTempBonusList() { return getSafeListFor(ListKey.TEMP_BONUS); } /** * Add to the list of temporary bonuses * @param aBonus */ public void addTempBonus(final BonusObj aBonus) { addToListFor(ListKey.TEMP_BONUS, aBonus); } /** * Remove from the list of temporary bonuses * @param aBonus */ public void removeTempBonus(final BonusObj aBonus) { removeFromListFor(ListKey.TEMP_BONUS, aBonus); } /** * Reset (Clear) the temporary bonus list */ public void resetTempBonusList() { removeListFor(ListKey.TEMP_BONUS); } public boolean altersAC(PlayerCharacter pc) { String alterAC = pc.getControl(CControl.ALTERSAC); if (alterAC != null) { Object o = pc.getLocal(this, alterAC); return ((Boolean) o).booleanValue(); } return getRawBonusList(pc).stream().anyMatch(bonus -> bonus.getBonusInfo().equalsIgnoreCase("AC")); } @Override public String[] getTypes() { String type = getType(); return type.split("\\."); } @Override public List<String> getTypesForDisplay() { List<Type> trueTypeList = getTrueTypeList(true); List<String> result = new ArrayList<>(trueTypeList.size()); trueTypeList.stream().map(Type::toString).forEach(result::add); return result; } /** * Retrieve the icon for this equipment item. This may be directly set for * the item, or it may be for one of the item's types. The types are * checked from right to left. * * @return The icon for this equipment item, or null if none */ @Override public File getIcon() { // Check for icon on this specific item URI uri = this.get(ObjectKey.ICON_URI); if (uri != null) { return new File(uri); } // If not defined, then try the types GameMode game = SettingsHandler.getGame(); List<String> typeList = typeList(true); String iconPath = null; int iconPriority = 0; for (String type : typeList) { String path = game.getEquipTypeIcon(type); if (path != null) { int priority = game.getEquipTypeIconPriority(type); // Later types will win priority ties if (iconPath == null || priority >= iconPriority) { iconPath = path; iconPriority = priority; } } } if (iconPath != null) { return new File(iconPath); } // A default fallback String path = game.getEquipTypeIcon(Constants.DEFAULT); if (path != null) { return new File(path); } // No icon can be found return null; } @Override public String getLocalScopeName() { return "PC.EQUIPMENT"; } public Object getLocalVariable(CharID id, String varName) { ResultFacet resultFacet = FacetLibrary.getFacet(ResultFacet.class); return resultFacet.getLocalVariable(id, this, varName); } @Override public CDOMObject getLocalChild(String childType, String childName) { if ("EQUIPMENT.PART".equals(childType)) { return getEquipmentHead(Integer.parseInt(childName)); } return null; } @Override public List<String> getChildTypes() { return Arrays.asList(new String[] { "EQUIPMENT.PART" }); } @Override public List<PCGenScoped> getChildren(String childType) { if ("EQUIPMENT.PART".equals(childType)) { return new ArrayList<>(heads); } return null; } }