/* * EquipmentPart.java * * Copyright (c) 2009 Jay Lawson <jaylawson39 at yahoo.com>. All rights reserved. * * This file is part of MekHQ. * * MekHQ is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * MekHQ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with MekHQ. If not, see <http://www.gnu.org/licenses/>. */ package mekhq.campaign.parts.equipment; import java.io.PrintWriter; import java.util.GregorianCalendar; import java.util.SortedSet; import java.util.TreeSet; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import megamek.common.Compute; import megamek.common.CriticalSlot; import megamek.common.Entity; import megamek.common.EquipmentType; import megamek.common.MiscType; import megamek.common.Mounted; import megamek.common.TechConstants; import megamek.common.WeaponType; import megamek.common.weapons.BayWeapon; import mekhq.MekHQ; import mekhq.MekHqXmlUtil; import mekhq.campaign.Campaign; import mekhq.campaign.parts.MissingPart; import mekhq.campaign.parts.Part; import mekhq.campaign.unit.Unit; import mekhq.campaign.universe.Era; /** * This part covers most of the equipment types in WeaponType, AmmoType, and MiscType * It can robustly handle all equipment with static weights and costs. It can also * handle equipment whose only variability in terms of cost is the equipment tonnage itself. * More complicated variable weight/cost equipment needs to be subclassed. * Some examples of equipment that needs to be subclasses: * - MASC (depends on engine rating) * - AES (depends on location and cost is by unit tonnage) * @author Jay Lawson <jaylawson39 at yahoo.com> */ public class EquipmentPart extends Part { private static final long serialVersionUID = 2892728320891712304L; //crap equipmenttype is not serialized! protected transient EquipmentType type; protected String typeName; protected int equipmentNum = -1; protected double equipTonnage; public EquipmentType getType() { return type; } public int getEquipmentNum() { return equipmentNum; } public void setEquipmentNum(int n) { this.equipmentNum = n; } public EquipmentPart() { this(0, null, -1, false, null); } public EquipmentPart(int tonnage, EquipmentType et, int equipNum, Campaign c) { this(0, et, equipNum, false, c); } public EquipmentPart(int tonnage, EquipmentType et, int equipNum, boolean omniPodded, Campaign c) { super(tonnage, omniPodded, c); this.type =et; if(null != type) { this.name = type.getName(); this.typeName = type.getInternalName(); } if (equipNum != -1) { this.equipmentNum = equipNum; } else { equipmentNum = -1; } if(null != type) { try { equipTonnage = type.getTonnage(null); } catch(NullPointerException ex) { //System.out.println("Found a null entity while calculating tonnage for " + name); } } } @Override public void setUnit(Unit u) { super.setUnit(u); if(null != unit) { equipTonnage = type.getTonnage(unit.getEntity()); } } public void setEquipTonnage(double ton) { equipTonnage = ton; } public EquipmentPart clone() { EquipmentPart clone = new EquipmentPart(getUnitTonnage(), type, equipmentNum, omniPodded, campaign); clone.copyBaseData(this); if(hasVariableTonnage(type)) { clone.setEquipTonnage(equipTonnage); } return clone; } @Override public double getTonnage() { return equipTonnage; } /** * Restores the equipment from the name */ public void restore() { if (typeName == null) { typeName = type.getName(); } else { type = EquipmentType.get(typeName); } if (type == null) { System.err .println("Mounted.restore: could not restore equipment type \"" + typeName + "\""); return; } try { equipTonnage = type.getTonnage(null); } catch(NullPointerException ex) { //System.out.println("Found a null entity while calculating tonnage for " + name); } } @Override public boolean isSamePartType(Part part) { //According to official answer, if sticker prices are different then //they are not acceptable substitutes, so we need to check for that as //well //http://bg.battletech.com/forums/strategic-operations/(answered)-can-a-lance-for-a-35-ton-mech-be-used-on-a-40-ton-mech-and-so-on/ return part instanceof EquipmentPart && getType().equals(((EquipmentPart)part).getType()) && getTonnage() == part.getTonnage() && getStickerPrice() == part.getStickerPrice() && isOmniPodded() == part.isOmniPodded(); } @Override public int getTechLevel() { int techLevel = getType().getTechLevel(campaign.getCalendar().get(GregorianCalendar.YEAR)); if(techLevel == TechConstants.T_TECH_UNKNOWN && !getType().getTechLevels().isEmpty()) { //If this is tech unknown we are probably using a part before its date of introduction //in this case, try to give it the date of the earliest entry if it exists SortedSet<Integer> keys = new TreeSet<Integer>(getType().getTechLevels().keySet()); techLevel = getType().getTechLevels().get(keys.first()); } if ((techLevel != TechConstants.T_ALLOWED_ALL && techLevel < 0) || techLevel >= TechConstants.T_ALL) return TechConstants.T_TECH_UNKNOWN; else return techLevel; } @Override public void writeToXml(PrintWriter pw1, int indent) { writeToXmlBegin(pw1, indent); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<equipmentNum>" +equipmentNum +"</equipmentNum>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<typeName>" +MekHqXmlUtil.escape(type.getInternalName()) +"</typeName>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<equipTonnage>" +equipTonnage +"</equipTonnage>"); writeToXmlEnd(pw1, indent); } @Override protected void loadFieldsFromXmlNode(Node wn) { NodeList nl = wn.getChildNodes(); for (int x=0; x<nl.getLength(); x++) { Node wn2 = nl.item(x); if (wn2.getNodeName().equalsIgnoreCase("equipmentNum")) { equipmentNum = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("typeName")) { typeName = wn2.getTextContent(); } else if (wn2.getNodeName().equalsIgnoreCase("equipTonnage")) { equipTonnage = Double.parseDouble(wn2.getTextContent()); } } restore(); } @Override public int getAvailability(int era) { return type.getAvailability(Era.convertEra(era)); } @Override public int getIntroDate() { if (isOmniPodded()) { if (isClanTechBase()) { return Math.max(2850, getType().getIntroductionDate()); } else { return Math.max(3052, getType().getIntroductionDate()); } } return getType().getIntroductionDate(); } @Override public int getExtinctDate() { return getType().getExtinctionDate(); } @Override public int getReIntroDate() { return getType().getReintruductionDate(); } @Override public int getTechRating() { return type.getTechRating(); } @Override public void fix() { super.fix(); if(null != unit) { Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted) { mounted.setHit(false); mounted.setMissing(false); mounted.setDestroyed(false); unit.repairSystem(CriticalSlot.TYPE_EQUIPMENT, equipmentNum); } checkWeaponBay(); } } @Override public MissingPart getMissingPart() { return new MissingEquipmentPart(getUnitTonnage(), type, equipmentNum, campaign, equipTonnage, omniPodded); } @Override public void remove(boolean salvage) { if(null != unit) { Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted) { mounted.setHit(true); mounted.setDestroyed(true); mounted.setRepairable(false); unit.destroySystem(CriticalSlot.TYPE_EQUIPMENT, equipmentNum); } Part spare = campaign.checkForExistingSparePart(this); if(!salvage) { campaign.removePart(this); } else if(null != spare) { int number = quantity; while(number > 0) { spare.incrementQuantity(); number--; } campaign.removePart(this); } unit.removePart(this); Part missing = getMissingPart(); unit.addPart(missing); campaign.addPart(missing, 0); } checkWeaponBay(); setUnit(null); updateConditionFromEntity(false); equipmentNum = -1; } @Override public void updateConditionFromEntity(boolean checkForDestruction) { if(null != unit) { int priorHits = hits; Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted) { if(mounted.isMissing()) { remove(false); return; } hits = unit.getEntity().getDamagedCriticals(CriticalSlot.TYPE_EQUIPMENT, equipmentNum, mounted.getLocation()); if(mounted.isSplit()) { hits += unit.getEntity().getDamagedCriticals(CriticalSlot.TYPE_EQUIPMENT, equipmentNum, mounted.getSecondLocation()); } omniPodded = mounted.isOmniPodMounted(); } if(checkForDestruction && hits > priorHits && Compute.d6(2) < campaign.getCampaignOptions().getDestroyPartTarget()) { remove(false); return; } } } @Override public int getBaseTime() { if(isSalvaging()) { return isOmniPodded()? 30 : 120; } if(hits == 1) { return 100; } else if(hits == 2) { return 150; } else if(hits == 3) { return 200; } else if(hits > 3) { return 250; } return 0; } @Override public int getDifficulty() { if(isSalvaging()) { return 0; } if(hits == 1) { return -3; } else if(hits == 2) { return -2; } else if(hits == 3) { return 0; } else if(hits > 3) { return 2; } return 0; } @Override public boolean needsFixing() { return hits > 0; } public int getLocation() { if(null != unit) { Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted) { return mounted.getLocation(); } } return -1; } public boolean isRearFacing() { if(null != unit) { Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted) { return mounted.isRearMounted(); } } return false; } @Override public void updateConditionFromPart() { if(null != unit) { Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted) { mounted.setMissing(false); if(hits >= 1) { mounted.setDestroyed(true); mounted.setHit(true); mounted.setRepairable(true); unit.damageSystem(CriticalSlot.TYPE_EQUIPMENT, equipmentNum, hits); } else { mounted.setHit(false); mounted.setDestroyed(false); mounted.setRepairable(true); unit.repairSystem(CriticalSlot.TYPE_EQUIPMENT, equipmentNum); } setOmniPodded(mounted.isOmniPodMounted()); } checkWeaponBay(); } } @Override public String checkFixable() { if(isSalvaging()) { return null; } // The part is only fixable if the location is not destroyed. // be sure to check location and second location if(null != unit) { Mounted m = unit.getEntity().getEquipment(equipmentNum); if(null != m) { int loc = m.getLocation(); if (unit.isLocationBreached(loc)) { return unit.getEntity().getLocationName(loc) + " is breached."; } if (unit.isLocationDestroyed(loc)) { return unit.getEntity().getLocationName(loc) + " is destroyed."; } loc = m.getSecondLocation(); if(loc != Entity.LOC_NONE) { if (unit.isLocationBreached(loc)) { return unit.getEntity().getLocationName(loc) + " is breached."; } if (unit.isLocationDestroyed(loc)) { return unit.getEntity().getLocationName(loc) + " is destroyed."; } } } } return null; } @Override public boolean isMountedOnDestroyedLocation() { // This should do the exact same as the big loop commented out below - Dylan try { return unit != null && unit.getEntity() != null && unit.getEntity().getEquipment(equipmentNum) != null && unit.isLocationDestroyed(unit.getEntity().getEquipment(equipmentNum).getLocation()); } catch (Exception e) { MekHQ.logError(e); } return false; /*if(null == unit) { return false; } for(int loc = 0; loc < unit.getEntity().locations(); loc++) { for (int i = 0; i < unit.getEntity().getNumberOfCriticals(loc); i++) { CriticalSlot slot = unit.getEntity().getCritical(loc, i); // ignore empty & system slots if ((slot == null) || (slot.getType() != CriticalSlot.TYPE_EQUIPMENT)) { continue; } Mounted equip = unit.getEntity().getEquipment(equipmentNum); Mounted m1 = slot.getMount(); Mounted m2 = slot.getMount2(); if (m1 == null && m2 == null) { continue; } if (slot.getIndex() == equipmentNum || (equip.equals(m1) || equip.equals(m2))) { return unit.isLocationDestroyed(loc); } } } return false;*/ } @Override public boolean onBadHipOrShoulder() { if(null != unit) { for(int loc = 0; loc < unit.getEntity().locations(); loc++) { for (int i = 0; i < unit.getEntity().getNumberOfCriticals(loc); i++) { CriticalSlot slot = unit.getEntity().getCritical(loc, i); // ignore empty & system slots if ((slot == null) || (slot.getType() != CriticalSlot.TYPE_EQUIPMENT)) { continue; } Mounted equip = unit.getEntity().getEquipment(equipmentNum); Mounted m1 = slot.getMount(); Mounted m2 = slot.getMount2(); if (m1 == null && m2 == null) { continue; } if ((equip.equals(m1)) || (equip.equals(m2))) { if (unit.hasBadHipOrShoulder(loc)) { return true; } } } } } return false; } /** * Copied from megamek.common.Entity.getWeaponsAndEquipmentCost(StringBuffer detail, boolean ignoreAmmo) * */ @Override public long getStickerPrice() { //OK, we cant use the resolveVariableCost methods from megamek, because they //rely on entity which may be null if this is a spare part. So we use our //own resolveVariableCost method //TODO: we need a static method that returns whether this equipment type depends upon // - unit tonnage // - item tonnage // - engine // use that to determine how to add things to the parts store and to // determine whether what can be used as a replacement //why does all the proto ammo have no cost? Entity en = null; boolean isArmored = false; double itemCost = type.getRawCost(); if (itemCost == EquipmentType.COST_VARIABLE) { itemCost = resolveVariableCost(isArmored); } if (unit != null) { en = unit.getEntity(); Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted) { isArmored = mounted.isArmored(); } type.getCost(en, isArmored, getLocation()); } if (isOmniPodded()) { itemCost *= 1.25; } int finalCost = (int)itemCost; if (isArmored) { //need a getCriticals command - but how does this work? //finalCost += 150000 * getCriticals(entity); } return finalCost; } private int resolveVariableCost(boolean isArmored) { double varCost = 0; Entity en = null; if (getUnit() != null) { en = getUnit().getEntity(); } if (en != null) { varCost = type.getCost(en, isArmored, getLocation()); } else if (type instanceof MiscType) { if (type.hasFlag(MiscType.F_DRONE_CARRIER_CONTROL)) { varCost = getTonnage() * 10000; } else if (type.hasFlag(MiscType.F_OFF_ROAD)) { varCost = 10 * getTonnage() * getTonnage(); } else if (type.hasFlag(MiscType.F_FLOTATION_HULL) || type.hasFlag(MiscType.F_VACUUM_PROTECTION) || type.hasFlag(MiscType.F_ENVIRONMENTAL_SEALING) || type.hasFlag(MiscType.F_OFF_ROAD)) { //?? } else if (type.hasFlag(MiscType.F_LIMITED_AMPHIBIOUS) || type.hasFlag((MiscType.F_FULLY_AMPHIBIOUS))) { varCost = getTonnage() * 10000; } else if (type.hasFlag(MiscType.F_DUNE_BUGGY)) { varCost = 10 * getTonnage() * getTonnage(); } else if (type.hasFlag(MiscType.F_MASC) && type.hasFlag(MiscType.F_BA_EQUIPMENT)) { //TODO: handle this one differently //costValue = entity.getRunMP() * 75000; } else if (type.hasFlag(MiscType.F_HEAD_TURRET) || type.hasFlag(MiscType.F_SHOULDER_TURRET) || type.hasFlag(MiscType.F_QUAD_TURRET)) { varCost = getTonnage() * 10000; } else if (type.hasFlag(MiscType.F_SPONSON_TURRET)) { varCost = getTonnage() * 4000; } else if (type.hasFlag(MiscType.F_PINTLE_TURRET)) { varCost = getTonnage() * 1000; } else if (type.hasFlag(MiscType.F_ARMORED_MOTIVE_SYSTEM)) { //TODO: handle this through motive system part varCost = getTonnage() * 100000; } else if (type.hasFlag(MiscType.F_JET_BOOSTER)) { //TODO: Handle this one through subtyping //varCost = entity.getEngine().getRating() * 10000; } else if (type.hasFlag(MiscType.F_DRONE_OPERATING_SYSTEM)) { varCost = (getTonnage() * 10000) + 5000; } else if (type.hasFlag(MiscType.F_TARGCOMP)) { varCost = getTonnage() * 10000; } else if (type.hasFlag(MiscType.F_CLUB) && (type.hasSubType(MiscType.S_HATCHET) || type.hasSubType(MiscType.S_MACE_THB))) { varCost = getTonnage() * 5000; } else if (type.hasFlag(MiscType.F_CLUB) && type.hasSubType(MiscType.S_SWORD)) { varCost = getTonnage() * 10000; } else if (type.hasFlag(MiscType.F_CLUB) && type.hasSubType(MiscType.S_RETRACTABLE_BLADE)) { varCost = (1 + getTonnage()) * 10000; } else if (type.hasFlag(MiscType.F_TRACKS)) { //TODO: Handle this through subtyping //varCost = (int) Math.ceil((500 * entity.getEngine().getRating() * entity.getWeight()) / 75); } else if (type.hasFlag(MiscType.F_TALON)) { varCost = (int) Math.ceil(getTonnage() * 300); } else if (type.hasFlag(MiscType.F_SPIKES)) { varCost = (int) Math.ceil(getTonnage() * 50); } else if (type.hasFlag(MiscType.F_PARTIAL_WING)) { varCost = (int) Math.ceil(getTonnage() * 50000); } else if (type.hasFlag(MiscType.F_ACTUATOR_ENHANCEMENT_SYSTEM)) { //TODO: subtype this one //int multiplier = entity.locationIsLeg(loc) ? 700 : 500; //costValue = (int) Math.ceil(entity.getWeight() * multiplier); } else if (type.hasFlag(MiscType.F_HAND_WEAPON) && (type.hasSubType(MiscType.S_CLAW))) { varCost = (int) Math.ceil(getUnitTonnage() * 200); } else if (type.hasFlag(MiscType.F_CLUB) && (type.hasSubType(MiscType.S_LANCE))) { varCost = (int) Math.ceil(getUnitTonnage() * 150); } } if (varCost == 0) { // if we don't know what it is... System.out.println("I don't know how much " + name + " costs."); } return (int) Math.ceil(varCost); } /* * The following static functions help the parts store determine how to handle * variable weight equipment. If the type returns true to hasVariableTonnage * then the parts store will use a for loop to create equipment of the given tonnage * using the other helper functions. Note that this should not be used for supclassed * equipment parts whose "uniqueness" depends on more than the item tonnage */ public static boolean hasVariableTonnage(EquipmentType type) { return (type instanceof MiscType && (type.hasFlag(MiscType.F_TARGCOMP) || type.hasFlag(MiscType.F_CLUB) || type.hasFlag(MiscType.F_TALON))); } public static double getStartingTonnage(EquipmentType type) { return 1; } public static double getMaxTonnage(EquipmentType type) { if (type.hasFlag(MiscType.F_TALON)|| (type.hasFlag(MiscType.F_CLUB) && (type.hasSubType(MiscType.S_HATCHET) || type.hasSubType(MiscType.S_MACE_THB)))) { return 7; } else if (type.hasFlag(MiscType.F_CLUB) && (type.hasSubType(MiscType.S_LANCE) || type.hasSubType(MiscType.S_SWORD))) { return 5; } else if (type.hasFlag(MiscType.F_CLUB) && type.hasSubType(MiscType.S_MACE)) { return 10; } else if (type.hasFlag(MiscType.F_CLUB) && type.hasSubType(MiscType.S_RETRACTABLE_BLADE)) { return 5.5; } else if (type.hasFlag(MiscType.F_TARGCOMP)) { //direct fire weapon weight divided by 4 - what is reasonably the highest - 15 tons? return 15; } return 1; } public static double getTonnageIncrement(EquipmentType type) { if((type.hasFlag(MiscType.F_CLUB) && type.hasSubType(MiscType.S_RETRACTABLE_BLADE))) { return 0.5; } return 1; } @Override public boolean isPartForEquipmentNum(int index, int loc) { return equipmentNum == index; } @Override public boolean isOmniPoddable() { if (type.isOmniFixedOnly()) { return false; } if (type instanceof MiscType) { return type.hasFlag(MiscType.F_MECH_EQUIPMENT) || type.hasFlag(MiscType.F_TANK_EQUIPMENT) || type.hasFlag(MiscType.F_AERO_EQUIPMENT); } else if (type instanceof WeaponType) { return (type.hasFlag(WeaponType.F_MECH_WEAPON) || type.hasFlag(WeaponType.F_TANK_WEAPON) || type.hasFlag(WeaponType.F_AERO_WEAPON)) && !((WeaponType)type).isCapital(); } return true; } @Override public String getLocationName() { if(null != unit) { Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted && mounted.getLocation() != -1) { return unit.getEntity().getLocationName(mounted.getLocation()); } } return null; } @Override public boolean isInLocation(String loc) { if(null == unit || null == unit.getEntity() || null == unit.getEntity().getEquipment(equipmentNum)) { return false; } Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null == mounted) { return false; } int location = unit.getEntity().getLocationFromAbbr(loc); for (int i = 0; i < unit.getEntity().getNumberOfCriticals(location); i++) { CriticalSlot slot = unit.getEntity().getCritical(location, i); // ignore empty & non-hittable slots if ((slot == null) || !slot.isEverHittable() || slot.getType()!=CriticalSlot.TYPE_EQUIPMENT || null == slot.getMount()) { continue; } if(unit.getEntity().getEquipmentNum(slot.getMount()) == equipmentNum) { return true; } } //if we are still here, lets just double check by the mounted's location and secondary location if(mounted.getLocation() == location) { return true; } if(location != Entity.LOC_NONE && mounted.getSecondLocation() == location) { return true; } return false; } /** * This method will check for an existing weapon bay that this equipment belongs to * and if there is one it will check the status of that weapon bay based on the equipment. * If this equipment is functional, then it will clear any hits from the bay. If not, then it will * check all the other equipment in the bay and if they are all damaged, then it will mark the bay * as destroyed. This is designed to be used only by the fix and remove methods contained here in * order to properly update weapon bay mounts on the entity */ private void checkWeaponBay() { if(type instanceof WeaponType && null != unit && null != unit.getEntity() && unit.getEntity().usesWeaponBays()) { Mounted weapon = unit.getEntity().getEquipment(equipmentNum); if(null == weapon) { return; } Mounted weaponBay = null; for(Mounted m : unit.getEntity().getWeaponBayList()) { if(m.getLocation() != weapon.getLocation()) { continue; } if(m.getType() instanceof BayWeapon && m.getBayWeapons().contains(equipmentNum)) { weaponBay = m; break; } } if(null == weaponBay) { return; } int wBayIndex = unit.getEntity().getEquipmentNum(weaponBay); //ok we found the weapons bay, now lets check first to see if the current weapon is fixed if(!weapon.isDestroyed()) { weaponBay.setHit(false); weaponBay.setMissing(false); weaponBay.setDestroyed(false); unit.repairSystem(CriticalSlot.TYPE_EQUIPMENT, wBayIndex); return; } //if we are still here then we need to check the other weapons, if any of them are usable //then we should do the same thing. Otherwise all weapons are destroyed and we should mark //the bay as unusuable for(int wId : weaponBay.getBayWeapons()) { Mounted m = unit.getEntity().getEquipment(wId); if(null == m) { continue; } if(!m.isDestroyed()) { weaponBay.setHit(false); weaponBay.setMissing(false); weaponBay.setDestroyed(false); unit.repairSystem(CriticalSlot.TYPE_EQUIPMENT, wBayIndex); return; } } weaponBay.setHit(true); weaponBay.setDestroyed(true); weaponBay.setRepairable(true); unit.destroySystem(CriticalSlot.TYPE_EQUIPMENT, wBayIndex); } } }