/* * EquipmentSetFacadeImpl.java * Copyright James Dempsey, 2010 * * 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 * * Created on 15/01/2011 3:17:17 PM * * $Id$ */ package pcgen.gui2.facade; import java.io.Serializable; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang3.StringUtils; import pcgen.cdom.base.Constants; import pcgen.cdom.enumeration.StringKey; import pcgen.cdom.facet.FacetLibrary; import pcgen.cdom.facet.analysis.HandsFacet; import pcgen.cdom.util.CControl; import pcgen.core.BodyStructure; import pcgen.core.Equipment; import pcgen.core.Globals; import pcgen.core.PlayerCharacter; import pcgen.core.SystemCollections; import pcgen.core.character.EquipSet; import pcgen.core.character.EquipSlot; import pcgen.core.display.CharacterDisplay; import pcgen.facade.core.BodyStructureFacade; import pcgen.facade.core.DataSetFacade; import pcgen.facade.core.EquipmentFacade; import pcgen.facade.core.EquipmentListFacade; import pcgen.facade.core.EquipmentListFacade.EquipmentListEvent; import pcgen.facade.core.EquipmentListFacade.EquipmentListListener; import pcgen.facade.core.EquipmentSetFacade; import pcgen.facade.core.EquipmentSetFacade.EquipNode.NodeType; import pcgen.facade.core.UIDelegate; import pcgen.facade.util.DefaultListFacade; import pcgen.facade.util.DefaultReferenceFacade; import pcgen.facade.util.ListFacade; import pcgen.facade.util.ReferenceFacade; import pcgen.facade.util.event.ListEvent; import pcgen.facade.util.event.ListListener; import pcgen.system.LanguageBundle; import pcgen.util.Logging; import pcgen.util.enumeration.Tab; /** * The Class {@code EquipmentSetFacadeImpl} is an implementation of * the EquipmentSetFacade interface for the new user interface. It handles * the interaction with the UI and the character with respect a grouping * of the character's gear. This covers what is carried, what is not and * where each item is located. As a result it also manages what items are * deemed active. * * @author James Dempsey <jdempsey@users.sourceforge.net> */ class EquipmentSetFacadeImpl implements EquipmentSetFacade, EquipmentListListener, ListListener<EquipmentFacade> { private final PlayerCharacter theCharacter; private final CharacterDisplay charDisplay; private EquipSet eqSet; private final UIDelegate delegate; private final TodoManager todoManager; private final DataSetFacade dataSet; private final EquipmentListFacadeImpl purchasedList; private final Collection<EquipmentTreeListener> listeners = new ArrayList<>(); private DefaultReferenceFacade<String> name; private EquipmentListFacadeImpl equippedItemsList; private double totalWeight = 0; /** This list of equipment nodes to be displayed on the equipped tree. */ private DefaultListFacade<EquipNode> nodeList; private Map<EquipSlot, EquipNode> equipSlotNodeMap; private Map<String, EquipNodeImpl> naturalWeaponNodes; /** List of phantom nodes which are currently both empty and not able to contain equipment */ private Set<EquipNodeImpl> hiddenPhantomNodes; private final CharacterFacadeImpl characterFacadeImpl; /** * Create a new Equipment Set Facade implementation for an existing * equipset. * * @param delegate The user interface delegate for notifying the user. * @param pc The character the set belongs to. * @param eqSet The set. * @param dataSet The datasets that the character is using. * @param purchasedList The list of the charcter's purchased equipment. * @param todoManager The user tasks tracker. * @param characterFacadeImpl The UI facade for the character. */ public EquipmentSetFacadeImpl(UIDelegate delegate, PlayerCharacter pc, EquipSet eqSet, DataSetFacade dataSet, EquipmentListFacadeImpl purchasedList, TodoManager todoManager, CharacterFacadeImpl characterFacadeImpl) { this.delegate = delegate; this.theCharacter = pc; this.todoManager = todoManager; this.characterFacadeImpl = characterFacadeImpl; this.charDisplay = pc.getDisplay(); this.dataSet = dataSet; this.purchasedList = purchasedList; initForEquipSet(eqSet); purchasedList.addEquipmentListListener(this); purchasedList.addListListener(this); } private void initForEquipSet(EquipSet equipSet) { this.eqSet = equipSet; name = new DefaultReferenceFacade<>(equipSet.getName()); equippedItemsList = new EquipmentListFacadeImpl(); naturalWeaponNodes = new HashMap<>(); hiddenPhantomNodes = new HashSet<>(); buildNodeList(); List<EquipSet> equipList = new ArrayList<>(charDisplay.getEquipSet()); Collections.sort(equipList); createNaturalWeaponSlots(); updateNaturalWeaponSlots(); updatePhantomSlots(); addChildrenToPath(equipSet.getIdPath(), equipList, null); } private void buildNodeList() { nodeList = new DefaultListFacade<>(); equipSlotNodeMap = new LinkedHashMap<>(); int index = 0; for (BodyStructureFacade bodyStruct : dataSet.getEquipmentLocations()) { String structString = bodyStruct.toString(); EquipNodeImpl node = new EquipNodeImpl((BodyStructure) bodyStruct, index++); nodeList.addElement(node); // Add locations for this body structure for (EquipSlot slot : SystemCollections.getUnmodifiableEquipSlotList()) { String bodyStructureName = slot.getBodyStructureName(); final String hands = "HANDS"; if ("Ring".equalsIgnoreCase(slot.getSlotName()) || "Fingers".equalsIgnoreCase(slot.getSlotName())) { bodyStructureName = hands; } if (bodyStructureName.equalsIgnoreCase(structString)) { if (slot.canContainType("WEAPON")) { // Add phantom nodes for the various weapon slots if (getPCHands() > 0) { addEquipNodeForEquipSlot( node, createWeaponEquipSlot(slot, Constants.EQUIP_LOCATION_PRIMARY), true); } for (int i = 1; i < getPCHands(); ++i) { if (i > 1) { addEquipNodeForEquipSlot( node, createWeaponEquipSlot(slot, Constants.EQUIP_LOCATION_SECONDARY + " " + i), true); } else { addEquipNodeForEquipSlot( node, createWeaponEquipSlot(slot, Constants.EQUIP_LOCATION_SECONDARY), true); } } addEquipNodeForEquipSlot(node, createWeaponEquipSlot( slot, Constants.EQUIP_LOCATION_DOUBLE), true); addEquipNodeForEquipSlot(node, createWeaponEquipSlot( slot, Constants.EQUIP_LOCATION_BOTH), true); addEquipNodeForEquipSlot(node, createWeaponEquipSlot( slot, Constants.EQUIP_LOCATION_UNARMED), true); } else { addEquipNodeForEquipSlot(node, slot, false); } } } } } private int getPCHands() { String solverValue = theCharacter.getControl(CControl.CREATUREHANDS); if (solverValue == null) { return FacetLibrary.getFacet(HandsFacet.class).getHands( theCharacter.getCharID()); } else { Object val = theCharacter.getGlobal(solverValue); return ((Number) val).intValue(); } } private EquipSlot createWeaponEquipSlot(EquipSlot slot, String slotName) { EquipSlot wpnSlot = slot.clone(); wpnSlot.setSlotName(slotName); return wpnSlot; } /** * Create a new EquipNodeImpl for the slot and add it the node list. * @param bodyStructNode The parent body structure node * @param slot The equipment slot * @param singleOnly Can the slot only ever have a single entry. e.g. weapon slots */ private void addEquipNodeForEquipSlot(EquipNodeImpl bodyStructNode, EquipSlot slot, boolean singleOnly) { EquipNodeImpl slotNode = new EquipNodeImpl(bodyStructNode, slot, singleOnly); nodeList.addElement(slotNode); equipSlotNodeMap.put(slot, slotNode); } /** * Recursive method to build up a tree of EquipNodes. It finds from the * equipment list those children of the item specified by the idPath and * adds them to the paths list. It will then do the same for each child * that was found. If the parent is null then all direct children will be * attached to the relevant body structure nodes. * * @param idPath The equipset id of the parent record. * @param equipList The list of equipsets to be added. * @param parent The parent node */ private void addChildrenToPath(String idPath, List<EquipSet> equipList, EquipNodeImpl parent) { List<EquipNodeImpl> children = new ArrayList<>(); // process all EquipNodeImpl Items for (int iSet = 0; iSet < equipList.size(); ++iSet) { EquipSet es = equipList.get(iSet); if (es.getParentIdPath().equals(idPath)) { EquipSlot slot = Globals.getEquipSlotByName(es.getName()); EquipNodeImpl slotNode = (EquipNodeImpl) equipSlotNodeMap.get(slot); EquipNodeImpl parentNode = parent; if (parentNode == null) { if (slotNode != null) { parentNode = (EquipNodeImpl) slotNode.getParent(); } else { for (EquipNode scanNode : nodeList) { if ((scanNode.getNodeType() == NodeType.BODY_SLOT) && scanNode.toString().equals(es.getName())) { parentNode = (EquipNodeImpl) scanNode; break; } if ((scanNode.getNodeType() == NodeType.PHANTOM_SLOT) && scanNode.toString().equals(es.getName())) { parentNode = (EquipNodeImpl) scanNode.getParent(); slotNode = (EquipNodeImpl) scanNode; slot = ((EquipNodeImpl) scanNode).getSlot(); break; } } } } if (parentNode != null) { EquipNodeImpl node = new EquipNodeImpl(parentNode, slot, es.getItem(), es.getIdPath()); nodeList.addElement(node); if ((slotNode != null) && (slotNode.getNodeType() == NodeType.PHANTOM_SLOT) && (getNumFreeSlots(slotNode) <= 0)) { nodeList.removeElement(slotNode); for (EquipNode inompatNode : getIncompatibleWeaponSlots(slotNode)) { nodeList.removeElement(inompatNode); } } updateTotalQuantity(es.getItem(), es.getItem().getQty() .intValue()); // updateTotalWeight(es.getItem(), es.getItem().getQty(), // parentNode.getBodyStructure()); // add to list for recursive calls children.add(node); } else { Logging.errorPrint("Could not find parent for " + es.getName() + " for item " + es.getItem() + " at path " + es.getIdPath()); } // and remove from tempSetList so // it won't get processed again equipList.remove(es); --iSet; } } // Now process the children for (final EquipNodeImpl equipNodeImpl : children) { addChildrenToPath(equipNodeImpl.getIdPath(), equipList, equipNodeImpl); } } /** * Make this equipment set the active one. */ void activateEquipSet() { theCharacter.setCalcEquipSetId(eqSet.getIdPath()); theCharacter.setCalcEquipmentList(); updateOutputOrder(); theCharacter.setUseTempMods(eqSet.getUseTempMods()); theCharacter.calcActiveBonuses(); theCharacter.setDirty(true); } /** * Add the weight for the item to the total for the set. * * @param equip The equipment item being added * @param quantity The number being added (or negative if being removed) * @param root The BodyStructure in which the item is being placed or removed */ private void updateTotalWeight(Equipment equip, float quantity, BodyStructureFacade root) { if (!Constants.EQUIP_LOCATION_NOTCARRIED.equals(root.toString())) { equip.setNumberCarried(equip.getCarried()+quantity); if (!Constants.EQUIP_LOCATION_CARRIED.equals(root.toString())) { equip.setNumberEquipped((int) (equip.getNumberEquipped()+quantity)); } } theCharacter.setCalcEquipmentList(); if (!Constants.EQUIP_LOCATION_NOTCARRIED.equals(root.toString())) { totalWeight = charDisplay.totalWeight(); } } /** * returns new id_Path with the last id one higher than the current * highest id for EquipSets with the same ParentIdPath. * TODO: This needs to be moved to the core. * @param display The display interface for the character owning the equipset. * @param parentSet The parent of the equipset that is being created, null if it a root set. * @return new id path **/ static String getNewIdPath(CharacterDisplay display, EquipSet parentSet) { String pid = "0"; int newID = 0; if (parentSet != null) { pid = parentSet.getIdPath(); } for (EquipSet es : display.getEquipSet()) { if (es.getParentIdPath().equals(pid) && (es.getId() > newID)) { newID = es.getId(); } } ++newID; NumberFormat format = (parentSet != null) ? new DecimalFormat("00") : new DecimalFormat("0"); return pid + '.' + format.format(newID); } /** * Shift the equipment sets down to make room for an item to be inserted. * @param parentSet The equipment set the item is being inserted into. * @param startingNode The first equipment node to be moved down. * @param origPathToNode A map of the equipment nodes to their original paths. * @param origPathToEquipSet A map of the equipment sets to their original paths. * @return The path which has been available. */ private String shiftEquipSetsDown( EquipSet parentSet, EquipNodeImpl startingNode, Map<String, EquipNodeImpl> origPathToNode, Map<String, EquipSet> origPathToEquipSet) { String pid = "0"; NumberFormat format = (parentSet != null) ? new DecimalFormat("00") : new DecimalFormat("0"); if (parentSet != null) { pid = parentSet.getIdPath(); } //Logging.errorPrint("Moving children of " + parentSet + " down, starting with " + startingNode + " ."); String startingPath = startingNode.idPath; int startingId = EquipSet.getIdFromPath(startingPath); for (Map.Entry<String, EquipNodeImpl> entry : origPathToNode.entrySet()) { String origPath = entry.getKey(); EquipNodeImpl node = entry.getValue(); EquipSet es = origPathToEquipSet.get(origPath); int esId = es.getId(); if (es.getParentIdPath().equals(pid)) { if (esId >= startingId) { esId++; } String newPath = pid + '.' + format.format(esId); es.setIdPath(newPath); updateContainerPath(origPath, newPath, origPathToNode, origPathToEquipSet); node.setIdPath(newPath); nodeList.modifyElement(node); } } String newPath = pid + '.' + format.format(startingId); return newPath; } /** * Update the path of any items contained within an equipment item being moved. * * @param parentOrigPath The original path of the container. * @param parentNewPath The new path of the container. * @param origPathToNode The map of the equipment nodes by path. * @param origPathToEquipSet The map of the equipment sets by path. */ private void updateContainerPath(String parentOrigPath, String parentNewPath, Map<String, EquipNodeImpl> origPathToNode, Map<String, EquipSet> origPathToEquipSet) { for (final Map.Entry<String, EquipSet> entry : origPathToEquipSet.entrySet()) { String origItemPath = entry.getKey(); EquipSet itemEs = entry.getValue(); if (origItemPath.startsWith(parentOrigPath) && !origItemPath.equals(parentOrigPath)) { String newItemPath = origItemPath.replace(parentOrigPath, parentNewPath); itemEs.setIdPath(newItemPath); EquipNodeImpl node = origPathToNode.get(origItemPath); if (node != null) { node.setIdPath(newItemPath); nodeList.modifyElement(node); } } } } /** * Create a map of the character's equipment nodes keyed on their current * id paths. * @return A map of id paths and the matching equipment nodes. */ private Map<String, EquipNodeImpl> buildPathNodeMap() { Map<String, EquipNodeImpl> pathMap = new HashMap<>(); for (EquipNode node : nodeList) { if ((node instanceof EquipNodeImpl) && (((EquipNodeImpl) node).getIdPath() != null)) { EquipNodeImpl eni = (EquipNodeImpl) node; pathMap.put(eni.idPath, eni); } } return pathMap; } /** * Create a map of the character's equipment sets keyed on their current * id paths. * @return A map of id paths and the matching equipment sets. */ private Map<String, EquipSet> buildPathEquipSetMap() { Map<String, EquipSet> esMap = new HashMap<>(); for (EquipSet es : charDisplay.getEquipSet()) { esMap.put(es.getIdPath(), es); } return esMap; } /* (non-Javadoc) * @see pcgen.core.facade.EquipmentSetFacade#addEquipment(pcgen.core.facade.EquipmentSetFacade.EquipNode, pcgen.core.facade.EquipmentFacade, int) */ @Override public EquipmentFacade addEquipment(EquipNode node, EquipmentFacade equipment, int quantity) { return addEquipment(node, equipment, quantity, null); } @Override public EquipmentFacade addEquipment(EquipNode node, EquipmentFacade equipment, int quantity, EquipNode beforeNode) { if (!(node instanceof EquipNodeImpl)) { return null; } if (!(equipment instanceof Equipment)) { return null; } Equipment item = (Equipment) equipment; EquipNodeImpl targetNode = (EquipNodeImpl) node; // Validate the item can go into the location. if (!canEquip(targetNode, equipment)) { delegate.showErrorMessage(Constants.APPLICATION_NAME, LanguageBundle .getFormattedString("in_equipCannotEquipToLocation", item .toString(), targetNode.toString())); return null; } EquipNodeImpl parent; EquipSet parentEs; EquipSlot equipSlot; String locName; switch (targetNode.getNodeType()) { case BODY_SLOT: parent = targetNode; parentEs = eqSet; equipSlot = null; locName = parent.toString(); break; case PHANTOM_SLOT: parent = (EquipNodeImpl) targetNode.getParent(); parentEs = eqSet; equipSlot = targetNode.getSlot(); locName = equipSlot.toString(); break; case EQUIPMENT: parent = targetNode; parentEs = charDisplay.getEquipSetByIdPath(parent.getIdPath()); equipSlot = targetNode.getSlot(); locName = parent.toString(); break; default: // Should never occur return null; } // Check for adding more instances to an existing item, but don't merge containers if (!item.isContainer()) { for (EquipNode existing : nodeList) { if (parent.equals(existing.getParent()) && (existing.getNodeType() == NodeType.EQUIPMENT)) { EquipNodeImpl existingImpl = (EquipNodeImpl) existing; if ((equipSlot != null) && !equipSlot.equals(existingImpl.getSlot())) { continue; } Equipment existingItem = (Equipment) existing.getEquipment(); if (existingItem.equals(item)) { int totalQuantity = (int) (existingItem.getQty() + quantity); existingItem.setQty(totalQuantity); EquipSet es = charDisplay .getEquipSetByIdPath(((EquipNodeImpl) existing) .getIdPath()); es.setQty(es.getQty() + quantity); updateTotalWeight(existingItem, quantity, parent.getBodyStructure()); fireQuantityChanged(existing); updateTotalQuantity(existingItem, quantity); updateNaturalWeaponSlots(); updatePhantomSlots(); return existingItem; } } } } // Create equip set for the item String id; if ((beforeNode != null) && (beforeNode instanceof EquipNodeImpl)) { id = shiftEquipSetsDown(parentEs, (EquipNodeImpl) beforeNode, buildPathNodeMap(), buildPathEquipSetMap()); } else { id = EquipmentSetFacadeImpl.getNewIdPath(charDisplay, parentEs); } Equipment newItem = item.clone(); EquipSet newSet = new EquipSet(id, locName, newItem.getName(), newItem); newItem.setQty(quantity); newSet.setQty((float) quantity); theCharacter.addEquipSet(newSet); Equipment eqTarget = parentEs.getItem(); if ((eqTarget != null) && eqTarget.isContainer()) { eqTarget.insertChild(theCharacter, newItem); newItem.setParent(eqTarget); } // Create EquipNode for the item EquipNodeImpl itemNode = new EquipNodeImpl(parent, equipSlot, newItem, id); nodeList.addElement(itemNode); if ((targetNode.getNodeType() == NodeType.PHANTOM_SLOT) && (getNumFreeSlots(targetNode) <= 0)) { nodeList.removeElement(targetNode); for (EquipNode inompatNode : getIncompatibleWeaponSlots(targetNode)) { nodeList.removeElement(inompatNode); } } updateTotalWeight(newItem, quantity, parent.getBodyStructure()); updateTotalQuantity(newItem, quantity); updateNaturalWeaponSlots(); updateOutputOrder(); theCharacter.calcActiveBonuses(); updatePhantomSlots(); characterFacadeImpl.postEquippingUpdates(); return newItem; } @Override public boolean moveEquipment(EquipNode node, int numRowsToMove) { // Confirm our assumptions if (!(node instanceof EquipNodeImpl) || !(node.getBodyStructure() instanceof BodyStructure) || (node.getNodeType() != NodeType.EQUIPMENT) || (node.getParent() == null)) { return false; } if (numRowsToMove == 0) { return true; } BodyStructure bodyStruct = (BodyStructure) node.getBodyStructure(); if (!bodyStruct.isHoldsAnyType()) { return false; } EquipNodeImpl equipNode = (EquipNodeImpl) node; List<EquipNode> orderedEquipNodes = new ArrayList<>( nodeList.getContents()); Collections.sort(orderedEquipNodes); // Get current location int currLoc = orderedEquipNodes.indexOf(node); if (currLoc < 0) { return false; } // Calculate new location EquipNodeImpl beforeNode; boolean addAsLastChildOfParent = false; if (numRowsToMove < 0) { beforeNode = scanBackForNewLoc(equipNode, orderedEquipNodes, numRowsToMove * -1, currLoc); } else { beforeNode = scanForwardForNewLoc(equipNode, orderedEquipNodes, numRowsToMove, currLoc); addAsLastChildOfParent = beforeNode == null; } // Move the equipment item Map<String, EquipNodeImpl> origPathToNode = buildPathNodeMap(); Map<String, EquipSet> origPathToEquipSet = buildPathEquipSetMap(); nodeList.removeElement(equipNode); String origIdPath = equipNode.getIdPath(); EquipSet parentEs = charDisplay.getEquipSetByIdPath(EquipSet .getParentPath(origIdPath)); EquipSet nodeEs = charDisplay.getEquipSetByIdPath(origIdPath); String newIdPath; if (addAsLastChildOfParent) { newIdPath = EquipmentSetFacadeImpl.getNewIdPath(charDisplay, parentEs); } else { newIdPath = shiftEquipSetsDown(parentEs, beforeNode, origPathToNode, origPathToEquipSet); } nodeEs.setIdPath(newIdPath); equipNode.setIdPath(newIdPath); nodeList.addElement(equipNode); // Update children of the item updateContainerPath(origIdPath, newIdPath, origPathToNode, origPathToEquipSet); return true; } @Override public boolean sortEquipment(EquipNode parentNode) { // Confirm our assumptions if (!(parentNode instanceof EquipNodeImpl) || !(parentNode.getBodyStructure() instanceof BodyStructure) || ((parentNode.getNodeType() != NodeType.EQUIPMENT) && (parentNode .getNodeType() != NodeType.BODY_SLOT))) { return false; } BodyStructure bodyStruct = (BodyStructure) parentNode.getBodyStructure(); if (!bodyStruct.isHoldsAnyType()) { return false; } String pid = ((EquipNodeImpl) parentNode).idPath; boolean isBodyStructure = parentNode.getBodyStructure() instanceof BodyStructure; List<EquipNodeImpl> childList = new ArrayList<>(); Map<String, EquipNodeImpl> origPathToNode = buildPathNodeMap(); Map<String, EquipSet> origPathToEquipSet = buildPathEquipSetMap(); for (Map.Entry<String, EquipNodeImpl> entry : origPathToNode.entrySet()) { final String origPath = entry.getKey(); final EquipNodeImpl node = entry.getValue(); EquipSet es = origPathToEquipSet.get(origPath); if (node.parent == parentNode) { childList.add(node); if (pid == null) { pid = es.getParentIdPath(); } } } // Sort child list childList.sort(new EquipNameComparator()); // Renumber paths // need to start from a unique id if only sorting some nodes at a level int id = isBodyStructure ? theCharacter.getNewChildId(pid) : 1; NumberFormat format = new DecimalFormat("00"); for (EquipNodeImpl childNode : childList) { String origPath = childNode.idPath; String newPath = pid + '.' + format.format(id); nodeList.removeElement(childNode); EquipSet es = origPathToEquipSet.get(origPath); es.setIdPath(newPath); updateContainerPath(origPath, newPath, origPathToNode, origPathToEquipSet); childNode.setIdPath(newPath); nodeList.addElement(childNode); id++; } return true; } /** * Scan back in the node list to find the row that is a certain number of * steps above the starting row. This will skip past the contents of any * containers which do not contain the start row. * * @param equipNode The node being moved. * @param orderedEquipNodes The sorted list of all equipment nodes for this equipment set. * @param numRowsToMove The positive number of rows to move back * @param startRow The row at which the node is currently located. * @return The node currently at the new lcoation. */ private EquipNodeImpl scanBackForNewLoc(EquipNodeImpl equipNode, List<EquipNode> orderedEquipNodes, int numRowsToMove, int startRow) { int currIndex = startRow; int numRowsMoved = 0; String lastIdPath = equipNode.getIdPath(); EquipNodeImpl lastRowNode = equipNode; while ((currIndex > 0) && (numRowsMoved < numRowsToMove)) { currIndex--; int lastDepth = EquipSet.getPathDepth(lastIdPath); EquipNodeImpl currRowNode = (EquipNodeImpl) orderedEquipNodes.get(currIndex); int currRowDepth = (currRowNode.getIdPath() == null) ? 0 : EquipSet .getPathDepth(currRowNode.getIdPath()); if (lastDepth < currRowDepth) { // Ignore this child of a higher container } else if ((equipNode.getBodyStructure() != currRowNode .getBodyStructure()) || (equipNode.getParent() != currRowNode.getParent())) { // We've gone too far (outside the target body structure or // past where the item can be equipped), so return the last item // we could equip to return lastRowNode; } else { // Valid target - parent or sibling of prev row numRowsMoved++; lastIdPath = currRowNode.getIdPath(); lastRowNode = currRowNode; } } return lastRowNode; } /** * Scan forward in the node list to find the row that is a certain number of * steps below the starting row. This will skip past the contents of any * containers which do not contain the start row. * * @param equipNode The node being moved. * @param orderedEquipNodes The sorted list of all equipment nodes for this equipment set. * @param numRowsToMove The positive number of rows to move forward * @param startRow The row at which the node is currently located. * @return The node currently at the new lcoation. */ private EquipNodeImpl scanForwardForNewLoc(EquipNodeImpl equipNode, List<EquipNode> orderedEquipNodes, int numRowsToMove, int startRow) { int currIndex = startRow; int numRowsMoved = 0; String lastIdPath = equipNode.getIdPath(); EquipNodeImpl lastRowNode = equipNode; while ((currIndex < orderedEquipNodes.size()) && (numRowsMoved <= numRowsToMove)) { currIndex++; if (currIndex == orderedEquipNodes.size()) { return null; } int lastDepth = EquipSet.getPathDepth(lastIdPath); EquipNodeImpl currRowNode = (EquipNodeImpl) orderedEquipNodes.get(currIndex); if (currRowNode.getIdPath() == null) { return null; } int currRowDepth = EquipSet.getPathDepth(currRowNode.getIdPath()); if (lastDepth < currRowDepth) { // Ignore this child of a lower container } else if ((equipNode.getBodyStructure() != currRowNode .getBodyStructure()) || (equipNode.getParent() != currRowNode.getParent())) { // We've gone too far (outside the target body structure or // past where the item can be equipped), so return the last item // we could equip to return null; } else { // Valid target - sibling of prev row or sibling of prev row's parent. numRowsMoved++; lastIdPath = currRowNode.getIdPath(); lastRowNode = currRowNode; } } return lastRowNode; } /** * Reorder the equipment for output to cater for any changes in the * equipment list. Note this assumes this equipment set is the active one * as it updates the character's master equipment list. */ private void updateOutputOrder() { List<EquipNode> orderedEquipNodes = new ArrayList<>( nodeList.getContents()); Collections.sort(orderedEquipNodes); List<Equipment> processed = new ArrayList<>(orderedEquipNodes.size()); int outputIndex = 1; for (EquipNode equipNode : orderedEquipNodes) { if (equipNode.getEquipment() != null) { Equipment equip = theCharacter.getEquipmentNamed(equipNode.getEquipment() .toString()); // If an item is split in multiple places, don't overwrite its order if ((equip != null) && !processed.contains(equip)) { equip.setOutputIndex(outputIndex++); processed.add(equip); } } } } /* (non-Javadoc) * @see pcgen.core.facade.EquipmentSetFacade#removeEquipment(pcgen.core.facade.EquipmentSetFacade.EquipNode, int) */ @Override public EquipmentFacade removeEquipment(EquipNode node, int quantity) { if (!(node instanceof EquipNodeImpl) || (node.getNodeType() != NodeType.EQUIPMENT)) { return null; } EquipNodeImpl targetNode = (EquipNodeImpl) node; EquipNodeImpl parentNode = (EquipNodeImpl) node.getParent(); EquipSet eSet = charDisplay.getEquipSetByIdPath(targetNode.getIdPath()); if (eSet == null) { Logging.errorPrint("No equipset found for node " + targetNode + " path " + targetNode.getIdPath()); return null; } int newQty = (int) (eSet.getQty()-quantity); Equipment eqI = eSet.getItem(); if (newQty <= 0) { // If it was a container, remove all children removeChildren(node); // remove Equipment (via EquipSet) from the PC theCharacter.delEquipSet(eSet); nodeList.removeElement(targetNode); // if it was inside a container, make sure to update // the container Equipment Object if (parentNode.getNodeType()==NodeType.EQUIPMENT) { Equipment eqP = eqI.getParent(); if (eqP != null) { eqP.removeChild(theCharacter, eqI); } } else if (targetNode.getSlot() != null) { final EquipNodeImpl restoredNode = (EquipNodeImpl) equipSlotNodeMap.get(targetNode.getSlot()); if ((restoredNode != null) && !nodeList.containsElement(restoredNode)) { nodeList.addElement(0, restoredNode); addCompatWeaponSlots(restoredNode); } } } else { eSet.setQty((float) newQty); fireQuantityChanged(targetNode); } updateTotalWeight(eqI, quantity*-1, targetNode.getBodyStructure()); updateTotalQuantity(eqI, quantity*-1); updateNaturalWeaponSlots(); theCharacter.calcActiveBonuses(); updatePhantomSlots(); return eqI; } /** * Remove all equipment items held within a supplied container * @param parentNode The container node to be emptied */ private void removeChildren(EquipNode parentNode) { if (!(parentNode instanceof EquipNodeImpl) || (parentNode.getNodeType() != NodeType.EQUIPMENT)) { return; } List<EquipNode> equipToBeRemoved = new ArrayList<>(nodeList.getSize()); for (EquipNode node : nodeList) { // Only select top level equipment, anything in a container will be removed along with the container. if ((node.getNodeType() == NodeType.EQUIPMENT) && (node.getParent() == parentNode)) { equipToBeRemoved.add(node); } } for (EquipNode node : equipToBeRemoved) { removeEquipment(node, getQuantity(node)); } } /** * Calculate a list of weapon slots that are not compatible with the * supplied slot. These can then be removed from the list to be displayed * when the slot is filled and added back in when the slot is empty. * * @param targetNode The node to be check. * @return The list of incompatible nodes, empty if the target is not a weapon slot. */ private List<EquipNode> getIncompatibleWeaponSlots(EquipNodeImpl targetNode) { List<EquipNode> wpnList = new ArrayList<>(); if (targetNode.getNodeType() != NodeType.PHANTOM_SLOT) { return wpnList; } String[] incompatLocNames = {}; final String slotName = targetNode.getSlot().toString(); if (Constants.EQUIP_LOCATION_PRIMARY.equals(slotName)) { incompatLocNames = new String[]{Constants.EQUIP_LOCATION_BOTH, Constants.EQUIP_LOCATION_DOUBLE}; } else if (Constants.EQUIP_LOCATION_SECONDARY.equals(slotName)) { incompatLocNames = new String[]{Constants.EQUIP_LOCATION_BOTH, Constants.EQUIP_LOCATION_DOUBLE, Constants.EQUIP_LOCATION_SHIELD}; } else if (Constants.EQUIP_LOCATION_SHIELD.equals(slotName)) { incompatLocNames = new String[]{Constants.EQUIP_LOCATION_BOTH, Constants.EQUIP_LOCATION_DOUBLE, Constants.EQUIP_LOCATION_SECONDARY}; } else if (Constants.EQUIP_LOCATION_BOTH.equals(slotName)) { incompatLocNames = new String[]{Constants.EQUIP_LOCATION_PRIMARY, Constants.EQUIP_LOCATION_DOUBLE, Constants.EQUIP_LOCATION_SECONDARY, Constants.EQUIP_LOCATION_SHIELD}; } else if (Constants.EQUIP_LOCATION_DOUBLE.equals(slotName)) { incompatLocNames = new String[]{Constants.EQUIP_LOCATION_PRIMARY, Constants.EQUIP_LOCATION_BOTH, Constants.EQUIP_LOCATION_SECONDARY, Constants.EQUIP_LOCATION_SHIELD}; } //TODO: Extra secondary locations for more than 2 arms List<String> namesList = Arrays.asList(incompatLocNames); for (EquipSlot slot : equipSlotNodeMap.keySet()) { if (namesList.contains(slot.toString())) { wpnList.add(equipSlotNodeMap.get(slot)); } } return wpnList; } /** * Add back to the list any weapon slots that are now valid again. Called * after a weapon is removed from a character and the weapon;s node has * been added back to the node list. * * @param restoredNode The weapon equip node being restored. */ private void addCompatWeaponSlots(final EquipNodeImpl restoredNode) { List<EquipNode> weaponSlots = getIncompatibleWeaponSlots(restoredNode); Set<EquipNode> incompatNodes = new HashSet<>(); for (EquipNode equipNode : nodeList) { if ((equipNode.getNodeType() == NodeType.EQUIPMENT) && affectsWeaponSlots(equipNode)) { EquipSlot slot = ((EquipNodeImpl) equipNode).getSlot(); if ((slot != null) && (equipSlotNodeMap.get(slot) != null)) { incompatNodes .addAll(getIncompatibleWeaponSlots((EquipNodeImpl) equipSlotNodeMap .get(slot))); } } } weaponSlots.removeAll(incompatNodes); for (EquipNode node : weaponSlots) { nodeList.addElement(0, node); } } private boolean affectsWeaponSlots(EquipNode equipNode) { List<String> typeList = Arrays.asList(equipNode.getEquipment().getTypes()); return typeList.contains("WEAPON") || typeList.contains("SHIELD"); } /** * Update the total quantity held of an item as recorded in the * equipmentList. This will add or remove the item from the list as * required. Items with the same name are aggregated to give a total * amount held. * * @param item The item to be updated * @param amtChanged The amount by which the quantity has changed. */ private void updateTotalQuantity(Equipment item, int amtChanged) { for (EquipmentFacade equip : equippedItemsList) { if (item.equals(equip)) { int newQty = equippedItemsList.getQuantity(equip) + amtChanged; if (newQty > 0) { equippedItemsList.setQuantity(equip, newQty); } else { equippedItemsList.removeElement(equip); } return; } } // Item is new so add it equippedItemsList.addElement(item, amtChanged); } /** * Update the available natural weapon slots that are displayed. Will hide the * natural weapon slots unless there are unequipped natural weapons. */ private void updateNaturalWeaponSlots() { if (pcHasUnequippedNaturalWeapons()) { // Ensure natural weapon locations are visible naturalWeaponNodes.values().stream() .filter(natWpnEquipNode -> !nodeList.containsElement(natWpnEquipNode)) .forEach(natWpnEquipNode -> nodeList.addElement(natWpnEquipNode) ); } else { // Ensure natural weapon locations are not visible naturalWeaponNodes.values().stream() .filter(natWpnEquipNode -> nodeList.containsElement(natWpnEquipNode)) .forEach(natWpnEquipNode -> nodeList.removeElement(natWpnEquipNode) ); } } /** * Examine each phantom slot and ensure its display status matches the * current free capacity. This may remove phantom slots from the node list * (where they are full), add them to the node list (where they have spare * capacity), or flag a todo (where they are over capacity. This is done to * react to bonus slots changing in the character. */ private void updatePhantomSlots() { // Check for phantom slots which are no longer usable and slots over capacity Set<EquipNode> nodesToBeRemoved = new HashSet<>(); Set<EquipNodeImpl> presentPNs = new HashSet<>(); Set<EquipNodeImpl> neededPNs = new HashSet<>(); for (EquipNode node : nodeList) { EquipNodeImpl nodeImpl = (EquipNodeImpl) node; switch (node.getNodeType()) { case PHANTOM_SLOT: if (getNumFreeSlots(node) <= 0) { nodesToBeRemoved.add(node); } else { presentPNs.add(nodeImpl); } break; case EQUIPMENT: if (nodeImpl.getSlot() != null) { final EquipNodeImpl parentNode = (EquipNodeImpl) equipSlotNodeMap.get(nodeImpl.getSlot()); if ((parentNode != null) && (parentNode.getNodeType() == NodeType.PHANTOM_SLOT)) { int numFreeSlots = getNumFreeSlots(parentNode); if (numFreeSlots < 0) { todoManager.addTodo(new TodoFacadeImpl( Tab.INVENTORY, parentNode.toString(), "in_equipNodeOverfull", Tab.EQUIPPING.name(), 9)); //$NON-NLS-1$ } else { todoManager.removeTodo("in_equipNodeOverfull", parentNode.toString()); //$NON-NLS-1$ if (numFreeSlots > 0) { neededPNs.add(parentNode); } } } } break; default: break; } } // Add hiddenPNs to neededPNs if they now have spare capacity for (EquipNode node : hiddenPhantomNodes) { if (getNumFreeSlots(node) > 0) { neededPNs.add((EquipNodeImpl) node); } } // Remove the phantom nodes flagged, add to hiddenPNs as needed for (EquipNode node : nodesToBeRemoved) { nodeList.removeElement(node); if (getQuantity(node) <= 0) { hiddenPhantomNodes.add((EquipNodeImpl) node); } } // Add any now needed phantom nodes to the visible list neededPNs.removeAll(presentPNs); for (EquipNodeImpl restoredNode : neededPNs) { nodeList.addElement(0, restoredNode); addCompatWeaponSlots(restoredNode); hiddenPhantomNodes.remove(restoredNode); } } /* (non-Javadoc) * @see pcgen.core.facade.EquipmentSetFacade#isContainer(pcgen.core.facade.EquipmentFacade) */ @Override public boolean isContainer(EquipmentFacade equipment) { if (!(equipment instanceof Equipment)) { return false; } Equipment item = (Equipment) equipment; return item.isContainer(); } /* (non-Javadoc) * @see pcgen.core.facade.EquipmentSetFacade#setName(java.lang.String) */ @Override public void setName(String name) { this.name.set(name); eqSet.setName(name); } /* (non-Javadoc) * @see pcgen.core.facade.EquipmentSetFacade#addEquipmentTreeListener(pcgen.core.facade.EquipmentSetFacade.EquipmentTreeListener) */ @Override public void addEquipmentTreeListener(EquipmentTreeListener listener) { listeners.add(listener); } /* (non-Javadoc) * @see pcgen.core.facade.EquipmentSetFacade#removeEquipmentTreeListener(pcgen.core.facade.EquipmentSetFacade.EquipmentTreeListener) */ @Override public void removeEquipmentTreeListener(EquipmentTreeListener listener) { listeners.remove(listener); } /* (non-Javadoc) * @see pcgen.core.facade.EquipmentSetFacade#getNameRef() */ @Override public ReferenceFacade<String> getNameRef() { return name; } @Override public EquipmentListFacade getEquippedItems() { return equippedItemsList; } /* (non-Javadoc) * @see pcgen.core.facade.EquipmentSetFacade#canEquip(pcgen.core.facade.EquipmentSetFacade.EquipNode, pcgen.core.facade.EquipmentFacade) */ @Override public boolean canEquip(EquipNode node, EquipmentFacade equipment) { if (!(equipment instanceof Equipment) || (node == null)) { return false; } Equipment item = (Equipment) equipment; // Check for a required location (i.e. you can't carry a natural weapon) EquipNode requiredLoc = getNaturalWeaponLoc(equipment); if (requiredLoc != null) { return validLocationForNaturalWeapon(node, item, requiredLoc); } // Is this a container? Then check if the object can fit in if (node.getNodeType() == NodeType.EQUIPMENT) { EquipmentFacade parent = node.getEquipment(); if ((parent instanceof Equipment) && ((Equipment) parent).isContainer()) { // Check if it fits if (((Equipment) parent).canContain(theCharacter, item) == 1) { return true; } } } if (node.getNodeType() == NodeType.PHANTOM_SLOT) { // Check first for an already full or taken slot if (!getNodes().containsElement(node)) { return false; } EquipSlot slot = ((EquipNodeImpl) node).getSlot(); if (slot.canContainType(item.getType())) { if (item.isWeapon()) { final String slotName = slot.getSlotName(); if (item.isUnarmed() && slotName.equals(Constants.EQUIP_LOCATION_UNARMED)) { return true; } if (item.isShield() && slotName.equals(Constants.EQUIP_LOCATION_SHIELD)) { return true; } // If it is outsized, they can't equip it to a weapon slot if (item.isWeaponOutsizedForPC(theCharacter)) { return false; } if (slotName.startsWith(Constants.EQUIP_LOCATION_BOTH)) { return true; } if (item.isMelee() && item.isDouble() && slotName.equals(Constants.EQUIP_LOCATION_DOUBLE)) { return true; } if (item.isWeaponOneHanded(theCharacter)) { if (slotName.equals(Constants.EQUIP_LOCATION_PRIMARY) || slotName.startsWith(Constants.EQUIP_LOCATION_SECONDARY)) { return true; } } } else { return true; } } } // Is this a body structure? Then check if the object be placed there if (node.getNodeType() == NodeType.BODY_SLOT) { BodyStructure root = (BodyStructure) node.getBodyStructure(); if (root.isHoldsAnyType()) { return !root.isForbidden(item.getTrueTypeList(false)); } } // This item can't be equipped in this location return false; } /** * Check if the node is a valid location for the natural weapon to be equipped to. * This allows primary natural weapons to be equipped to primary or secondary * slots, but secondary weapons only too the secondary slot. * * @param node The node to be tested. * @param equipment The natural weapon * @param naturalLoc The natural weapon;s preferred slot. * @return true if the node can take the natural weapon, false otherwise. */ private boolean validLocationForNaturalWeapon(EquipNode node, Equipment equipment, EquipNode naturalLoc) { if (equipment.isPrimaryNaturalWeapon()) { return naturalWeaponNodes.containsValue(node); } return node.equals(naturalLoc); } /* (non-Javadoc) * @see pcgen.core.facade.EquipmentSetFacade#getPreferredLoc(pcgen.core.facade.EquipmentFacade) */ @Override public String getPreferredLoc(EquipmentFacade equipment) { EquipNode reqNode = getNaturalWeaponLoc(equipment); if (reqNode != null) { return reqNode.toString(); } for (EquipNode node : equipSlotNodeMap.values()) { if (node.getNodeType()==NodeType.PHANTOM_SLOT) { if (canEquip(node, equipment)) { return node.toString(); } } } return LanguageBundle.getString("in_other"); //$NON-NLS-1$ } /** * Retrieve the preferred location for a natural weapon. Will return null * for non natural weapon equipment items. * * @param equipment The equipment item to be checked. * @return The preferred natural equip node, or null if not applicable. */ protected EquipNode getNaturalWeaponLoc(EquipmentFacade equipment) { if (!(equipment instanceof Equipment) || (equipment == null)) { return null; } Equipment item = (Equipment) equipment; String locName = theCharacter.getNaturalWeaponLocation(item); if (locName != null) { return naturalWeaponNodes.get(locName); } return null; } private void createNaturalWeaponSlots() { for (EquipSlot slot : SystemCollections.getUnmodifiableEquipSlotList()) { if (slot.canContainType("WEAPON")) //$NON-NLS-1$ { for (EquipNode node : nodeList) { if ((node.getNodeType() == NodeType.BODY_SLOT) && slot.getBodyStructureName().equalsIgnoreCase( node.getBodyStructure().toString())) { createNaturalWeaponSlot(slot, node, Constants.EQUIP_LOCATION_NATURAL_PRIMARY); createNaturalWeaponSlot(slot, node, Constants.EQUIP_LOCATION_NATURAL_SECONDARY); return; } } } } } private void createNaturalWeaponSlot(EquipSlot slot, EquipNode node, String locName) { EquipSlot natWpnEquipSlot = createWeaponEquipSlot(slot, locName); EquipNodeImpl slotNode = new EquipNodeImpl((EquipNodeImpl) node, natWpnEquipSlot, true); naturalWeaponNodes.put(locName, slotNode); } /** * Calculate the number of free instances of the slot there are in the * equipment set. * * @param slot The slot to be checked, must be a PHANTOM_SLOT. * @return The number of slots free */ private int getNumFreeSlots(EquipNode slot) { if (slot.getNodeType() != EquipNode.NodeType.PHANTOM_SLOT) { return 0; } EquipNodeImpl node = (EquipNodeImpl) slot; int numPossible = getQuantity(node); // Scan for items int numUsed = 0; for (EquipNode item : nodeList) { if ((item.getNodeType() == NodeType.EQUIPMENT) && (((EquipNodeImpl) item).getSlot() == node.getSlot())) { Equipment equip = (Equipment) item.getEquipment(); numUsed += equip.getSlots(theCharacter); } } return numPossible - numUsed; } /* (non-Javadoc) * @see pcgen.core.facade.EquipmentSetFacade#removeAllEquipment() */ @Override public void removeAllEquipment() { List<EquipNode> equipToBeRemoved = new ArrayList<>(nodeList.getSize()); for (EquipNode node : nodeList) { // Only select top level equipment, anything in a container will be removed along with the container. if ((node.getNodeType() == NodeType.EQUIPMENT) && (node.getParent().getNodeType() != NodeType.EQUIPMENT)) { equipToBeRemoved.add(node); } } for (EquipNode node : equipToBeRemoved) { removeEquipment(node, getQuantity(node)); } } @Override public String toString() { return name.get(); } /** * Notify any listeners that the quantity of a node has changed. * * @param node The node that has changed quantity. */ private void fireQuantityChanged(EquipNode node) { EquipmentTreeEvent event = null; for (EquipmentTreeListener equipmentTreeListener : listeners) { if (event == null) { event = new EquipmentTreeEvent(this, node); } equipmentTreeListener.quantityChanged(event); } } /** * @return The core EquipSet that this facade represents. */ EquipSet getEquipSet() { return eqSet; } @Override public ListFacade<EquipNode> getNodes() { return nodeList; } @Override public int getQuantity(EquipNode equipNode) { EquipNodeImpl node = (EquipNodeImpl) equipNode; switch (node.getNodeType()) { case BODY_SLOT: return 1; case PHANTOM_SLOT: final String slotName = node.getSlot().toString(); if (Constants.EQUIP_LOCATION_BOTH.equals(slotName)) { return 1; // Set to 1, we should only have 1 object here. - Andrew } if (Constants.EQUIP_LOCATION_DOUBLE.equals(slotName)) { return 1; // Set to 1, we should only have 1 object here. - Andrew } return node.singleOnly ? 1 : (node.getSlot().getSlotCount() + (int) theCharacter.getTotalBonusTo("SLOTS", node .getSlot().getSlotName())); default: EquipSet parentEs = charDisplay.getEquipSetByIdPath(node.getIdPath()); return (parentEs == null) ? 0 : parentEs.getQty().intValue(); } } /* (non-Javadoc) * @see pcgen.core.facade.EquipmentSetFacade#getLocation(pcgen.core.facade.EquipmentSetFacade.EquipNode) */ @Override public String getLocation(EquipNode equipNode) { EquipNodeImpl node = (EquipNodeImpl) equipNode; switch (node.getNodeType()) { case BODY_SLOT: return node.toString(); default: if (node.getSlot() != null) { return node.getSlot().toString(); } return node.getBodyStructure().toString(); } } @Override public String getLocation(EquipmentFacade equip) { for (EquipNode node : nodeList) { if (equip.equals(node.getEquipment())) { switch (node.getNodeType()) { case EQUIPMENT: case PHANTOM_SLOT: return getLocation(node); default: return node.getParent().toString(); } } } return getPreferredLoc(equip); } /** * Identify if the character has any natural weapons that have not been * equipped yet. * @return true if there are unequipped natural attacks, false if not. */ private boolean pcHasUnequippedNaturalWeapons() { for (Equipment equipItem : theCharacter.getEquipmentMasterList()) { if (equipItem.isNatural() && !equippedItemsList.containsElement(equipItem)) { return true; } } return false; } /** * The Class {@code EquipNodeImpl} represents a node in the equipping * tree. It may be an item of equipment or a slot that may be filled. * EquipNodeImpl objects are immutable. */ static class EquipNodeImpl implements EquipNode { private static final NumberFormat FMT = new DecimalFormat("00"); private final NodeType nodeType; private final BodyStructure bodyStructure; private Equipment equipment; private EquipNodeImpl parent; private final String name; private EquipSlot slot; private String idPath; private String order; private boolean singleOnly = false; /** * Create a new EquipNodeImpl instance representing a body structure. * * @param bodyStructure The part of the body. * @param order The order of the body structure */ public EquipNodeImpl(BodyStructure bodyStructure, int order) { this.nodeType = NodeType.BODY_SLOT; this.bodyStructure = bodyStructure; this.name = bodyStructure.toString(); this.order = FMT.format(order); } /** * Create a new EquipNodeImpl instance representing an equipment slot. * These are called phantom slots as they represent a location yet to * be filled. * * @param parent The parent body structure node * @param slot The equipment slot * @param singleOnly Can the slot only ever have a single entry. e.g. weapon slots */ public EquipNodeImpl(EquipNodeImpl parent, EquipSlot slot, boolean singleOnly) { this.nodeType = NodeType.PHANTOM_SLOT; this.bodyStructure = parent.bodyStructure; this.slot = slot; this.name = slot.getSlotName(); this.parent = parent; this.singleOnly = singleOnly; } /** * Create a new EquipNodeImpl instance representing an item of equipment. * * @param parent The parent node, may be null for a body structure. * @param slot The equipment slot the item is equipped to. * @param equipment The equipment item, may be null. * @param idPath The id of the path as used by the core. */ public EquipNodeImpl(EquipNodeImpl parent, EquipSlot slot, Equipment equipment, String idPath) { this.nodeType = NodeType.EQUIPMENT; this.bodyStructure = parent.bodyStructure; this.slot = slot; this.equipment = equipment; this.idPath = idPath; this.parent = parent; this.name = equipment.getDisplayName(); } @Override public BodyStructureFacade getBodyStructure() { return bodyStructure; } @Override public EquipmentFacade getEquipment() { return equipment; } @Override public NodeType getNodeType() { return nodeType; } @Override public EquipNode getParent() { return parent; } /** * @return the slot */ EquipSlot getSlot() { return slot; } /** * @return the idPath */ String getIdPath() { return idPath; } /** * @param idPath the idPath to set */ void setIdPath(String idPath) { this.idPath = idPath; } /** * @return The key to be used for sorting EquipNodes */ String getSortKey() { StringBuilder sortKey = new StringBuilder(50); if (parent != null) { sortKey.append(parent.getSortKey()); } switch (nodeType) { case BODY_SLOT: sortKey.append(order); break; case PHANTOM_SLOT: sortKey.append("|"); sortKey.append(slot.getSlotName()); break; case EQUIPMENT: sortKey.append("|"); String objKey = equipment.get(StringKey.SORT_KEY); if (objKey == null) { objKey = equipment.getDisplayName(); } sortKey.append(objKey); break; } return sortKey.toString(); } @Override public String toString() { return name; } @Override public int compareTo(EquipNode o) { if (o instanceof EquipNodeImpl) { EquipNodeImpl other = (EquipNodeImpl) o; String orderThis = getOrder(this); String orderOther = getOrder(other); if (!orderThis.equals(orderOther)) { return orderThis.compareTo(orderOther); } if ((getIdPath() != null) && (other.getIdPath() != null)) { return idPath.compareTo(other.idPath); } return getSortKey().compareTo(other.getSortKey()); } return toString().compareTo(o.toString()); } /** * Retrieve the order of the node, that is the top level * order of the body structures. * @param equipNodeImpl The node to be examined. * @return The order applicable to the node. */ private String getOrder(EquipNodeImpl equipNodeImpl) { if (StringUtils.isNotBlank(equipNodeImpl.order)) { return equipNodeImpl.order; } EquipNodeImpl enParent = equipNodeImpl.parent; if (enParent != null) { return getOrder(enParent); } return ""; } } @Override public void elementAdded(ListEvent<EquipmentFacade> e) { // We don't care about new equipment being added. } @Override public void elementRemoved(ListEvent<EquipmentFacade> e) { EquipmentFacade equipmentFacade = e.getElement(); if (equippedItemsList.containsElement(equipmentFacade)) { if (Logging.isDebugMode()) { Logging.debugPrint("Currently equipped item " + equipmentFacade + " is being removed."); } } List<EquipNodeImpl> affectedList = findEquipmentNodes(equipmentFacade); for (EquipNodeImpl equipNode : affectedList) { EquipSet eSet = charDisplay.getEquipSetByIdPath(equipNode.getIdPath()); if (eSet != null) { removeEquipment(equipNode, eSet.getQty().intValue()); } } } private List<EquipNodeImpl> findEquipmentNodes(EquipmentFacade equipmentFacade) { List<EquipNodeImpl> affectedList = new ArrayList<>(); for (EquipNode node : nodeList) { if (equipmentFacade.equals(node.getEquipment())) { affectedList.add((EquipNodeImpl) node); } } return affectedList; } @Override public void elementsChanged(ListEvent<EquipmentFacade> e) { // We expect a refresh of the equipment set to follow an equipment changed // event, so we can ignore these. if (Logging.isDebugMode()) { Logging.debugPrint("Equip elementsChanged " + e); } } @Override public void elementModified(ListEvent<EquipmentFacade> e) { // We don't care about new equipment being modified. } @Override public void quantityChanged(EquipmentListEvent e) { EquipmentFacade equipmentFacade = e.getEquipment(); if (equippedItemsList.containsElement(equipmentFacade)) { int quantity = purchasedList.getQuantity(equipmentFacade) - equippedItemsList.getQuantity(equipmentFacade); if (quantity < 0) { if (Logging.isDebugMode()) { Logging.debugPrint("Currently equipped item " + equipmentFacade + " is being partially removed " + quantity + " from " + equippedItemsList.getQuantity(equipmentFacade)); } int numStillToRemove = -1*quantity; List<EquipNodeImpl> affectedList = findEquipmentNodes(equipmentFacade); affectedList.sort(new EquipLocImportantComparator()); // TODO: Custom sort order for (EquipNodeImpl equipNode : affectedList) { EquipSet eSet = charDisplay.getEquipSetByIdPath(equipNode.getIdPath()); if (eSet != null) { int numToRemove = Math.min(eSet.getQty().intValue(), numStillToRemove); removeEquipment(equipNode, numToRemove); numStillToRemove -= numToRemove; } if (numStillToRemove <= 0) { return; } } } } } /** * The Class {@code EquipLocImportantComparator} compares EquipNodes based * on their 'importance' to the character. The predefined order of the slots is * not carried, carried, equipped, all others in alpha order. */ private static class EquipLocImportantComparator implements Comparator<EquipNodeImpl>, Serializable { @Override public int compare(EquipNodeImpl o1, EquipNodeImpl o2) { BodyStructureFacade bodyStruct1 = o1.getBodyStructure(); BodyStructureFacade bodyStruct2 = o2.getBodyStructure(); if (bodyStruct1 != bodyStruct2) { final String[] locOrder = {Constants.EQUIP_LOCATION_NOTCARRIED, Constants.EQUIP_LOCATION_CARRIED, Constants.EQUIP_LOCATION_EQUIPPED}; for (String locName : locOrder) { if (locName.equals(bodyStruct1.toString())) { return -1; } if (locName.equals(bodyStruct2.toString())) { return 1; } } return bodyStruct1.toString().compareTo(bodyStruct2.toString()); } return o1.compareTo(o2); } } /** * The Class {@code EquipNameComparator} compares EquipNodes based * on alpha order by sort key (if defined) or name. */ private static class EquipNameComparator implements Comparator<EquipNodeImpl>, Serializable { @Override public int compare(final EquipNodeImpl o1, final EquipNodeImpl o2) { return o1.getSortKey().compareTo(o2.getSortKey()); } } }