/* * Part.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; import java.io.PrintWriter; import java.io.Serializable; import java.util.ArrayList; import java.util.Hashtable; import java.util.StringJoiner; import java.util.UUID; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import megamek.common.Entity; import megamek.common.EquipmentType; import megamek.common.Tank; import megamek.common.TargetRoll; import megamek.common.TechConstants; import megamek.common.WeaponType; import mekhq.MekHQ; import mekhq.MekHqXmlSerializable; import mekhq.MekHqXmlUtil; import mekhq.Version; import mekhq.campaign.Campaign; import mekhq.campaign.parts.equipment.EquipmentPart; import mekhq.campaign.parts.equipment.MissingEquipmentPart; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.SkillType; import mekhq.campaign.unit.Unit; import mekhq.campaign.work.IAcquisitionWork; import mekhq.campaign.work.IPartWork; import mekhq.campaign.work.WorkTime; /** * Parts do the lions share of the work of repairing, salvaging, reloading, refueling, etc. * for units. Each unit has an ArrayList of all its relevant parts. There is a corresponding unit * variable in part but this can be null when we are dealing with a spare part, so when putting in * calls to unit, you should always check to make sure it is not null. * * There are two kinds of parts: Part and MissingPart. The latter is used as a placeholder on a unit to * indicate it is missing the given part. When parts are removed from a unit, they shold be replaced * with the appropriate missing part which will remind MHQ that a replacement needs to be done. * * Parts implement IPartWork and MissingParts also implement IAcquisitionWork. These interfaces allow for * most of the actual work that can be done on parts. There is a lot of variability in how parts actually handle * this work * @author Jay Lawson <jaylawson39 at yahoo.com> */ public abstract class Part implements Serializable, MekHqXmlSerializable, IPartWork { private static final long serialVersionUID = 6185232893259168810L; public static final int PART_TYPE_ARMOR = 0; public static final int PART_TYPE_WEAPON = 1; public static final int PART_TYPE_AMMO = 2; public static final int PART_TYPE_EQUIPMENT_PART = 3; public static final int PART_TYPE_MEK_ACTUATOR = 4; public static final int PART_TYPE_MEK_ENGINE = 5; public static final int PART_TYPE_MEK_GYRO = 6; public static final int PART_TYPE_MEK_LIFE_SUPPORT = 7; public static final int PART_TYPE_MEK_BODY_PART = 8; public static final int PART_TYPE_MEK_SENSOR = 9; public static final int PART_TYPE_GENERIC_SPARE_PART = 10; public static final int PART_TYPE_OTHER = 11; public static final int PART_TYPE_MEK_COCKPIT = 12; public static final int PART_TYPE_OMNI_SPACE = 13; public static final int T_UNKNOWN = -1; public static final int T_BOTH = 0; public static final int T_IS = 1; public static final int T_CLAN = 2; public static final int QUALITY_A = 0; public static final int QUALITY_B = 1; public static final int QUALITY_C = 2; public static final int QUALITY_D = 3; public static final int QUALITY_E = 4; public static final int QUALITY_F = 5; public interface REPAIR_PART_TYPE { public static final int ARMOR = 0; public static final int AMMO = 1; public static final int WEAPON = 2; public static final int GENERAL_LOCATION = 3; public static final int ENGINE = 4; public static final int GYRO = 5; public static final int ACTUATOR = 6; public static final int ELECTRONICS = 7; public static final int GENERAL = 8; public static final int HEATSINK = 9; public static final int MEK_LOCATION = 10; public static final int PHYSICAL_WEAPON = 11; public static final int POD_SPACE = 12; } private static final String[] partTypeLabels = { "Armor", "Weapon", "Ammo", "Equipment Part", "Mek Actuator", "Mek Engine", "Mek Gyro", "Mek Life Support", "Mek Body Part", "Mek Sensor", "Generic Spare Part", "Other", "Mek Cockpit", "Pod Space" }; public static String[] getPartTypeLabels() { return partTypeLabels; } protected String name; protected int id; //this is the unitTonnage which needs to be tracked for some parts //even when off the unit. actual tonnage is returned via the //getTonnage() method protected int unitTonnage; protected boolean omniPodded; //hits to this part protected int hits; //Taharqa: as of 8/12/2015, we are no longer going to track difficulty and time //as hard coded numbers but rather use abstract methods that get them from each part //depending on the dynamic characteristics of the part // the skill modifier for difficulty //protected int difficulty; // the amount of time for the repair (this is the base time) //protected int time; // time spent on the task so far for tasks that span days protected int timeSpent; // the minimum skill level in order to attempt protected int skillMin; //current repair mode for part protected WorkTime mode; protected UUID teamId; private boolean isTeamSalvaging; //null is valid. It indicates parts that are not attached to units. protected Unit unit; protected UUID unitId; protected int quality; protected boolean brandNew; //we need to keep track of a couple of potential mods that result from carrying //over a task, otherwise people can get away with working over time with no consequence protected boolean workingOvertime; protected int shorthandedMod; //this tracks whether the part is reserved for a refit protected UUID refitId; protected UUID reserveId; //for delivery protected int daysToArrival; //all parts need a reference to campaign protected Campaign campaign; /* * This will be unusual but in some circumstances certain parts will be linked to other parts. * These linked parts will be considered integral and subsidary to those other parts and will * not show up independently. Currently (8/8/2015), we are only using this for BA suits * We need a parent part id and a vector of children parts to represent this. */ protected int parentPartId; protected ArrayList<Integer> childPartIds; /** * The number of parts in exactly the same condition, * to track multiple spare parts more efficiently and also the shopping list */ protected int quantity; //reverse-compatability protected int oldUnitId = -1; protected int oldTeamId = -1; protected int oldRefitId = -1; //only relevant for acquisitionable parts protected int daysToWait; protected int replacementId; public Part() { this(0, false, null); } public Part(int tonnage, Campaign c) { this(tonnage, false, c); } public Part(int tonnage, boolean omniPodded, Campaign c) { this.name = "Unknown"; this.unitTonnage = tonnage; this.omniPodded = omniPodded; this.hits = 0; this.skillMin = SkillType.EXP_GREEN; this.mode = WorkTime.NORMAL; this.timeSpent = 0; this.unitId = null; this.workingOvertime = false; this.shorthandedMod = 0; this.refitId = null; this.daysToArrival = 0; this.campaign = c; this.brandNew = true; this.quantity = 1; this.replacementId = -1; this.quality = QUALITY_D; this.parentPartId = -1; this.childPartIds = new ArrayList<Integer>(); this.isTeamSalvaging = false; } public static String getQualityName(int quality, boolean reverse) { switch(quality) { case QUALITY_A: if(reverse) { return "F"; } return "A"; case QUALITY_B: if(reverse) { return "E"; } return "B"; case QUALITY_C: if(reverse) { return "D"; } return "C"; case QUALITY_D: if(reverse) { return "C"; } return "D"; case QUALITY_E: if(reverse) { return "B"; } return "E"; case QUALITY_F: if(reverse) { return "A"; } return "F"; default: return "?"; } } public String getQualityName() { return getQualityName(getQuality(), campaign.getCampaignOptions().reverseQualityNames()); } public void setId(int id) { this.id = id; } public int getId() { return id; } public UUID getUnitId() { return unitId; } public void setCampaign(Campaign c) { this.campaign = c; } public Campaign getCampaign() { return campaign; } public String getName() { return name; } /** * Sticker price is the value of the part according to the rulebooks * @return */ public abstract long getStickerPrice(); /** * This is the actual value of the part as affected by any characteristics * of the part itself * @return */ public long getCurrentValue() { return getStickerPrice(); } /** * This is the value of the part that may be affected by campaign options * @return */ public long getActualValue() { return adjustCostsForCampaignOptions(getCurrentValue()); } public boolean isPriceAdustedForAmount() { return false; } protected long adjustCostsForCampaignOptions(long cost) { if(getTechBase() == T_CLAN) { cost *= campaign.getCampaignOptions().getClanPriceModifier(); } if(needsFixing() && !isPriceAdustedForAmount()) { cost *= campaign.getCampaignOptions().getDamagedPartsValue(); //TODO: parts that cant be fixed should also be further reduced in price } else if(!isBrandNew()) { cost *= campaign.getCampaignOptions().getUsedPartsValue(getQuality()); } return cost; } public boolean isBrandNew() { return brandNew; } public void setBrandNew(boolean b) { this.brandNew = b; } public int getUnitTonnage() { return unitTonnage; } public abstract double getTonnage(); public boolean isOmniPodded() { return omniPodded; } public void setOmniPodded(boolean omniPod) { this.omniPodded = omniPod; } public Unit getUnit() { return unit; } public void setUnit(Unit u) { this.unit = u; if(null != unit) { unitId = unit.getId(); unitTonnage = (int) u.getEntity().getWeight(); } else { unitId = null; } } public String getStatus() { String toReturn = "Functional"; if(needsFixing()) { toReturn = "Damaged"; } if(isReservedForRefit()) { toReturn = "Reserved for Refit"; } if(isReservedForReplacement()) { toReturn = "Reserved for Repair"; } if(isBeingWorkedOn()) { toReturn = "Being worked on"; } if(!isPresent()) { //toReturn = "" + getDaysToArrival() + " days to arrival"; String dayName = "day"; if(getDaysToArrival() > 1) { dayName += "s"; } toReturn = "In transit (" + getDaysToArrival() + " " + dayName + ")"; } return toReturn; } public int getHits() { return hits; } public String getDesc() { String bonus = getAllMods(null).getValueAsString(); if (getAllMods(null).getValue() > -1) { bonus = "+" + bonus; } bonus = "(" + bonus + ")"; String toReturn = "<html><font size='2'"; String action = "Repair "; if(isSalvaging()) { action = "Salvage "; } String scheduled = ""; if (getTeamId() != null) { scheduled = " (scheduled) "; } toReturn += ">"; toReturn += "<b>" + action + getName() + "</b><br/>"; toReturn += getDetails() + "<br/>"; if(getSkillMin() > SkillType.EXP_ELITE) { toReturn += "<font color='red'>Impossible</font>"; } else { toReturn += "" + getTimeLeft() + " minutes" + scheduled; if(!getCampaign().getCampaignOptions().isDestroyByMargin()) { toReturn += ", " + SkillType.getExperienceLevelName(getSkillMin()); } toReturn += " " + bonus; if (getMode() != WorkTime.NORMAL) { toReturn += "<br/><i>" + getCurrentModeName() + "</i>"; } } toReturn += "</font></html>"; return toReturn; } public String getRepairDesc() { String toReturn = ""; if(needsFixing()) { String scheduled = ""; if (getTeamId() != null) { scheduled = " (scheduled) "; } String bonus = getAllMods(null).getValueAsString(); if (getAllMods(null).getValue() > -1) { bonus = "+" + bonus; } bonus = "(" + bonus + ")"; toReturn += getTimeLeft() + " minutes" + scheduled; toReturn += ", " + SkillType.getExperienceLevelName(getSkillMin()); toReturn += " " + bonus; if (getMode() != WorkTime.NORMAL) { toReturn += ", " + getCurrentModeName(); } } return toReturn; } public abstract int getTechRating(); public abstract int getAvailability(int era); public int getTechBase() { if(getTechLevel() == TechConstants.T_ALLOWED_ALL) { return T_BOTH; } if (getTechLevel() == TechConstants.T_TECH_UNKNOWN) { return T_UNKNOWN; } if(isClanTechBase()) { return T_CLAN; } else { return T_IS; } } public String getTechBaseName() { return getTechBaseName(getTechBase()); } public static String getTechBaseName(int base) { switch(base) { case T_BOTH: return "IS/Clan"; case T_CLAN: return "Clan"; case T_IS: return "IS"; case T_UNKNOWN: return "UNKNOWN"; default: return "??"; } } abstract public int getTechLevel(); abstract public int getIntroDate(); abstract public int getExtinctDate(); abstract public int getReIntroDate(); /** * We are going to only limit parts by year if they totally haven't been produced * otherwise, we will just replace the existing availability code with X */ public boolean isIntroducedBy(int year) { if (year < getIntroDate()) { return false; } return true; } public boolean isExtinctIn(int year) { if ((getExtinctDate() == EquipmentType.DATE_NONE)) { return false; } if (year >= getExtinctDate() && year < getReIntroDate()) { return true; } return false; } /** * Checks if the current part is exactly the "same kind" of part as the part * given in argument. This is used to determine whether we need to add new spare * parts, or increment existing ones. * * @param part * The part to be compared with the current part */ public boolean isSamePartTypeAndStatus(Part part) { return isSamePartType(part) && isSameStatus(part); } public abstract boolean isSamePartType(Part part); public boolean isSameStatus(Part part) { //parts that are reserved for refit or being worked on are never the same status if(isReservedForRefit() || isBeingWorkedOn() || isReservedForReplacement() || hasParentPart() || part.isReservedForRefit() || part.isBeingWorkedOn() || part.isReservedForReplacement() || part.hasParentPart()) { return false; } return quality == part.getQuality() && hits == part.getHits() && part.getSkillMin() == this.getSkillMin() && this.getDaysToArrival() == part.getDaysToArrival(); } protected boolean isClanTechBase() { return TechConstants.isClan(getTechLevel()); } public abstract void writeToXml(PrintWriter pw1, int indent); protected void writeToXmlBegin(PrintWriter pw1, int indent) { pw1.println(MekHqXmlUtil.indentStr(indent) + "<part id=\"" +id +"\" type=\"" +this.getClass().getName() +"\">"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<id>" +this.id +"</id>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<name>" +MekHqXmlUtil.escape(name) +"</name>"); if (omniPodded) { pw1.println(MekHqXmlUtil.indentStr(indent+1) + "<omniPodded/>"); } pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<unitTonnage>" +unitTonnage +"</unitTonnage>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<hits>" +hits +"</hits>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<timeSpent>" +timeSpent +"</timeSpent>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<mode>" +mode +"</mode>"); if(null != teamId) { pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<teamId>" +teamId.toString() +"</teamId>"); } pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<skillMin>" +skillMin +"</skillMin>"); if(null != unitId) { pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<unitId>" +unitId.toString() +"</unitId>"); } pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<workingOvertime>" +workingOvertime +"</workingOvertime>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<shorthandedMod>" +shorthandedMod +"</shorthandedMod>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<refitId>" +refitId +"</refitId>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<daysToArrival>" +daysToArrival +"</daysToArrival>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<brandNew>" +brandNew +"</brandNew>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<quantity>" +quantity +"</quantity>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<daysToWait>" +daysToWait +"</daysToWait>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<replacementId>" +replacementId +"</replacementId>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<quality>" +quality +"</quality>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<isTeamSalvaging>" +isTeamSalvaging +"</isTeamSalvaging>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<parentPartId>" +parentPartId +"</parentPartId>"); for(int childId : childPartIds) { pw1.println(MekHqXmlUtil.indentStr(indent + 1) + "<childPartId>" + childId + "</childPartId>"); } } protected void writeToXmlEnd(PrintWriter pw1, int indent) { pw1.println(MekHqXmlUtil.indentStr(indent) + "</part>"); } public static Part generateInstanceFromXML(Node wn, Version version) { Part retVal = null; NamedNodeMap attrs = wn.getAttributes(); Node classNameNode = attrs.getNamedItem("type"); String className = classNameNode.getTextContent(); //reverse compatability checks if(className.equalsIgnoreCase("mekhq.campaign.parts.MekEngine")) { className = "mekhq.campaign.parts.EnginePart"; } else if(className.equalsIgnoreCase("mekhq.campaign.parts.MissingMekEngine")) { className = "mekhq.campaign.parts.MissingEnginePart"; } else if(className.equalsIgnoreCase("mekhq.campaign.parts.EquipmentPart")) { className = "mekhq.campaign.parts.equipment.EquipmentPart"; } else if(className.equalsIgnoreCase("mekhq.campaign.parts.MissingEquipmentPart")) { className = "mekhq.campaign.parts.equipment.MissingEquipmentPart"; } else if(className.equalsIgnoreCase("mekhq.campaign.parts.AmmoBin")) { className = "mekhq.campaign.parts.equipment.AmmoBin"; } else if(className.equalsIgnoreCase("mekhq.campaign.parts.MissingAmmoBin")) { className = "mekhq.campaign.parts.equipment.MissingAmmoBin"; } else if(className.equalsIgnoreCase("mekhq.campaign.parts.JumpJet")) { className = "mekhq.campaign.parts.equipment.JumpJet"; } else if(className.equalsIgnoreCase("mekhq.campaign.parts.MissingJumpJet")) { className = "mekhq.campaign.parts.equipment.MissingJumpJet"; } else if(className.equalsIgnoreCase("mekhq.campaign.parts.HeatSink")) { className = "mekhq.campaign.parts.equipment.HeatSink"; } else if(className.equalsIgnoreCase("mekhq.campaign.parts.MissingHeatSink")) { className = "mekhq.campaign.parts.equipment.MissingHeatSink"; } try { // Instantiate the correct child class, and call its parsing function. retVal = (Part) Class.forName(className).newInstance(); retVal.loadFieldsFromXmlNode(wn); // Okay, now load Part-specific fields! NodeList nl = wn.getChildNodes(); for (int x=0; x<nl.getLength(); x++) { Node wn2 = nl.item(x); if (wn2.getNodeName().equalsIgnoreCase("id")) { retVal.id = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("name")) { retVal.name = wn2.getTextContent(); } else if (wn2.getNodeName().equalsIgnoreCase("unitTonnage")) { retVal.unitTonnage = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("omniPodded")) { retVal.omniPodded = true; } else if (wn2.getNodeName().equalsIgnoreCase("quantity")) { retVal.quantity = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("hits")) { retVal.hits = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("timeSpent")) { retVal.timeSpent = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("skillMin")) { retVal.skillMin = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("mode")) { retVal.mode = WorkTime.of(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("daysToWait")) { retVal.daysToWait = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("teamId")) { if (version.getMajorVersion() == 0 && version.getMinorVersion() < 2 && version.getSnapshot() < 14) { retVal.oldTeamId = Integer.parseInt(wn2.getTextContent()); } else { if(!wn2.getTextContent().equals("null")) { retVal.teamId = UUID.fromString(wn2.getTextContent()); } } } else if (wn2.getNodeName().equalsIgnoreCase("unitId")) { if (version.getMajorVersion() == 0 && version.getMinorVersion() < 2 && version.getSnapshot() < 14) { retVal.oldUnitId = Integer.parseInt(wn2.getTextContent()); } else { if(!wn2.getTextContent().equals("null")) { retVal.unitId = UUID.fromString(wn2.getTextContent()); } } } else if (wn2.getNodeName().equalsIgnoreCase("shorthandedMod")) { retVal.shorthandedMod = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("refitId")) { if (version.getMajorVersion() == 0 && version.getMinorVersion() < 2 && version.getSnapshot() < 14) { retVal.oldRefitId = Integer.parseInt(wn2.getTextContent()); } else { if(!wn2.getTextContent().equals("null")) { retVal.refitId = UUID.fromString(wn2.getTextContent()); } } } else if (wn2.getNodeName().equalsIgnoreCase("daysToArrival")) { retVal.daysToArrival = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("workingOvertime")) { if(wn2.getTextContent().equalsIgnoreCase("true")) { retVal.workingOvertime = true; } else { retVal.workingOvertime = false; } } else if (wn2.getNodeName().equalsIgnoreCase("isTeamSalvaging")) { if(wn2.getTextContent().equalsIgnoreCase("true")) { retVal.isTeamSalvaging = true; } else { retVal.isTeamSalvaging = false; } } else if (wn2.getNodeName().equalsIgnoreCase("brandNew")) { if(wn2.getTextContent().equalsIgnoreCase("true")) { retVal.brandNew = true; } else { retVal.brandNew = false; } } else if (wn2.getNodeName().equalsIgnoreCase("replacementId")) { retVal.replacementId = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("quality")) { retVal.quality = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("parentPartId")) { retVal.parentPartId = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("childPartId")) { retVal.childPartIds.add(Integer.parseInt(wn2.getTextContent())); } } } catch (Exception ex) { // Errrr, apparently either the class name was invalid... // Or the listed name doesn't exist. // Doh! MekHQ.logError(ex); } // Refit protection of unit id if (retVal.unitId != null && retVal.refitId != null) { retVal.setUnit(null); } return retVal; } protected abstract void loadFieldsFromXmlNode(Node wn); @Override public int getActualTime() { return (int) Math.ceil(getBaseTime() * mode.timeMultiplier); } @Override public int getTimeLeft() { return getActualTime() - getTimeSpent(); } @Override public int getTimeSpent() { return timeSpent; } public void addTimeSpent(int m) { this.timeSpent += m; } public void resetTimeSpent() { this.timeSpent = 0; } public void resetOvertime() { this.workingOvertime = false; } @Override public int getSkillMin() { return skillMin; } public void setSkillMin(int i) { this.skillMin = i; } public WorkTime getMode() { return mode; } public void setMode(WorkTime wt) { if (canChangeWorkMode()) { this.mode = wt; } else { this.mode = WorkTime.NORMAL; } } @Override public boolean canChangeWorkMode() { return !(isOmniPodded() && isSalvaging()); } @Override public TargetRoll getAllMods(Person tech) { int difficulty = getDifficulty(); if (isOmniPodded() && (isSalvaging() || this instanceof MissingPart) && !(unit.getEntity() instanceof Tank)) { difficulty -= 2; } TargetRoll mods = new TargetRoll(difficulty, "difficulty"); int modeMod = mode.getMod(campaign.getCampaignOptions().isDestroyByMargin()); if (modeMod != 0) { mods.addModifier(modeMod, getCurrentModeName()); } if(null != unit) { mods.append(unit.getSiteMod()); if(unit.getEntity().hasQuirk("easy_maintain")) { mods.addModifier(-1, "easy to maintain"); } else if(unit.getEntity().hasQuirk("difficult_maintain")) { mods.addModifier(1, "difficult to maintain"); } } if(isClanTechBase() || (this instanceof MekLocation && this.getUnit() != null && this.getUnit().getEntity().isClan())) { if (null != tech && !tech.isClanner() && !tech.getSpas().containsKey("clan_tech_knowledge")) { mods.addModifier(2, "clan tech"); } } String qualityName = getQualityName(quality, campaign.getCampaignOptions().reverseQualityNames()); switch(quality) { case QUALITY_A: mods.addModifier(3, qualityName); break; case QUALITY_B: mods.addModifier(2, qualityName); break; case QUALITY_C: mods.addModifier(1, qualityName); break; case QUALITY_D: mods.addModifier(0, qualityName); break; case QUALITY_E: mods.addModifier(-1, qualityName); break; case QUALITY_F: mods.addModifier(-2, qualityName); break; } return mods; } public TargetRoll getAllModsForMaintenance() { //according to StratOps you get a -1 mod when checking on individual parts //but we will make this user customizable TargetRoll mods = new TargetRoll(campaign.getCampaignOptions().getMaintenanceBonus(), "maintenance"); mods.addModifier(Availability.getTechModifier(getTechRating()), "tech rating " + EquipmentType.getRatingName(getTechRating())); if(null != unit) { mods.append(unit.getSiteMod()); if(unit.getEntity().hasQuirk("easy_maintain")) { mods.addModifier(-1, "easy to maintain"); } else if(unit.getEntity().hasQuirk("difficult_maintain")) { mods.addModifier(1, "difficult to maintain"); } } if(isClanTechBase() || (this instanceof MekLocation && this.getUnit() != null && this.getUnit().getEntity().isClan())) { if (campaign.getPerson(getTeamId()) == null) { mods.addModifier(2, "clan tech"); } else if (!campaign.getPerson(getTeamId()).isClanner()) { mods.addModifier(2, "clan tech"); } } if(campaign.getCampaignOptions().useQualityMaintenance()) { switch(quality) { case QUALITY_A: mods.addModifier(3, "Quality A"); break; case QUALITY_B: mods.addModifier(2, "Quality B"); break; case QUALITY_C: mods.addModifier(1, "Quality C"); break; case QUALITY_D: mods.addModifier(0, "Quality D"); break; case QUALITY_E: mods.addModifier(-1, "Quality E"); break; case QUALITY_F: mods.addModifier(-2, "Quality F"); break; } } return mods; } public String getCurrentModeName() { return mode.name; } @Override public UUID getTeamId() { return teamId; } @Override public void setTeamId(UUID i) { //keep track of whether this was a salvage operation //because the entity may change if(null == i) { this.isTeamSalvaging = false; } else if(null == teamId) { this.isTeamSalvaging = isSalvaging(); } this.teamId = i; } public boolean isTeamSalvaging() { return null != getTeamId() && isTeamSalvaging; } public void setReserveId(UUID i) { this.reserveId = i; } @Override public String getPartName() { return name; } @Override public int getMassRepairOptionType() { return REPAIR_PART_TYPE.GENERAL; } @Override public int getRepairPartType() { return getMassRepairOptionType(); } @Override public void fix() { hits = 0; skillMin = SkillType.EXP_GREEN; shorthandedMod = 0; mode = WorkTime.NORMAL; } @Override public String fail(int rating) { skillMin = ++rating; timeSpent = 0; shorthandedMod = 0; return " <font color='red'><b> failed.</b></font>"; } @Override public String succeed() { if(isSalvaging()) { remove(true); return " <font color='green'><b> salvaged.</b></font>"; } else { fix(); return " <font color='green'><b> fixed.</b></font>"; } } @Override public String getDetails() { StringJoiner sj = new StringJoiner(", "); if (getLocationName() != null) { sj.add(getLocationName()); } if (isOmniPodded()) { sj.add("OmniPod"); } sj.add(hits + " hit(s)"); return sj.toString(); } @Override public boolean isSalvaging() { if(null != unit) { return unit.isSalvage() || isMountedOnDestroyedLocation() || isTeamSalvaging(); } return false; } public String checkScrappable() { return null; } public boolean canNeverScrap() { return false; } public String scrap() { remove(false); return getName() + " scrapped."; } @Override public boolean hasWorkedOvertime() { return workingOvertime; } @Override public void setWorkedOvertime(boolean b) { workingOvertime = b; } @Override public int getShorthandedMod() { return shorthandedMod; } @Override public void setShorthandedMod(int i) { shorthandedMod = i; } @Override public abstract Part clone(); protected void copyBaseData(Part part) { this.mode = part.mode; this.hits = part.hits; this.brandNew = part.brandNew; this.omniPodded = part.omniPodded; } public void setRefitId(UUID rid) { refitId = rid; } public UUID getRefitId() { return refitId; } public boolean isReservedForRefit() { return refitId != null; } public boolean isReservedForReplacement() { return reserveId != null; } public void setDaysToArrival(int days) { daysToArrival = days; } public int getDaysToArrival() { return daysToArrival; } public boolean checkArrival() { if(daysToArrival > 0) { daysToArrival--; return (daysToArrival == 0); } return false; } public boolean isPresent() { return daysToArrival == 0; } public boolean isBeingWorkedOn() { return teamId != null; } public void fixIdReferences(Hashtable<Integer, UUID> uHash, Hashtable<Integer, UUID> pHash) { unitId = uHash.get(oldUnitId); refitId = uHash.get(oldRefitId); teamId = pHash.get(oldTeamId); } /* public void resetRepairStatus() { if(null != unit) { setSalvaging(unit.isSalvage()); updateConditionFromEntity(false); } } */ public boolean onBadHipOrShoulder() { return false; } public boolean isMountedOnDestroyedLocation() { return false; } public boolean isPartForEquipmentNum(int index, int loc) { return false; } public boolean isInSupply() { return true; } public int getQuantity() { return quantity; } public void incrementQuantity() { quantity++; } public void decrementQuantity() { quantity--; if(quantity <= 0) { for(int childId : childPartIds) { Part p = campaign.getPart(childId); if(null != p) { campaign.removePart(p); } } campaign.removePart(this); } } public boolean isSpare() { return null == unitId && parentPartId == -1; } public boolean isRightTechType(String skillType) { return true; } public boolean isOmniPoddable() { return false; } public int getDaysToWait() { return daysToWait; } public void resetDaysToWait() { this.daysToWait = campaign.getCampaignOptions().getWaitingPeriod(); } public void decrementDaysToWait() { if(daysToWait > 0) { daysToWait--; } } public String getShoppingListReport(int quan) { return getQuantityName(quan) + ((quan > 1) ? " have " : " has ") + "been added to the procurement list."; } public String getArrivalReport() { return getQuantityName(quantity) + ((quantity > 1) ? " have " : " has ") + "arrived"; } public String getQuantityName(int quantity) { String answer = "" + quantity + " " + getName(); if(quantity > 1) { answer += "s"; } return answer; } /** Get the acquisition work to acquire a new part of this type * For most parts this is just getMissingPart(), but some override it * @return */ public IAcquisitionWork getAcquisitionWork() { return getMissingPart(); } public void doMaintenanceDamage(int d) { hits += d; updateConditionFromPart(); updateConditionFromEntity(false); } public int getQuality() { return quality; } public void improveQuality() { quality += 1; } public void decreaseQuality() { quality -= 1; } public void setQuality(int q) { quality = q; } public boolean needsMaintenance() { return true; } public void cancelAssignment() { setTeamId(null); resetOvertime(); resetTimeSpent(); setShorthandedMod(0); } public abstract String getLocationName(); public void setParentPartId(int id) { parentPartId = id; } public int getParentPartId() { return parentPartId; } public boolean hasParentPart() { return parentPartId != -1; } public ArrayList<Integer> getChildPartIds() { return childPartIds; } public void addChildPart(Part child) { childPartIds.add(child.getId()); child.setParentPartId(id); } public void removeChildPart(int childId) { ArrayList<Integer> tempArray = new ArrayList<Integer>(); for(int cid : childPartIds) { if(cid == childId) { Part part = campaign.getPart(childId); if(null != part) { part.setParentPartId(-1); } } else { tempArray.add(cid); } } childPartIds = tempArray; } public void removeAllChildParts() { for(int childId : childPartIds) { Part part = campaign.getPart(childId); if(null != part) { part.setParentPartId(-1); } } childPartIds = new ArrayList<Integer>(); } /** * Reserve a part for overnight work */ @Override public void reservePart() { //nothing goes here for real parts. Only missing parts need to reserve a replacement } @Override public void cancelReservation() { //nothing goes here for real parts. Only missing parts need to reserve a replacement } /** * Make any changes to the part needed for adding to the campaign */ public void postProcessCampaignAddition() { //do nothing } public boolean isInLocation(String loc) { if (null == unit || null == unit.getEntity()) { return false; } if (loc.equals("FSLG")) { return getLocation() == Entity.LOC_NONE; } return getLocation() == getUnit().getEntity().getLocationFromAbbr(loc); } @Override public String toString() { StringBuilder sb = new StringBuilder(getName()); sb.append(" "); //$NON-NLS-1$ sb.append(getDetails()); sb.append(", q: "); //$NON-NLS-1$ sb.append(quantity); if(null != unit) { sb.append(", mounted: "); //$NON-NLS-1$ sb.append(unit); } return sb.toString(); } public static String getRepairTypeShortName(int type) { switch (type) { case Part.REPAIR_PART_TYPE.ARMOR: return "Armor"; case Part.REPAIR_PART_TYPE.AMMO: return "Ammo"; case Part.REPAIR_PART_TYPE.WEAPON: return "Weapons"; case Part.REPAIR_PART_TYPE.GENERAL_LOCATION: return "Locations"; case Part.REPAIR_PART_TYPE.ENGINE: return "Engines"; case Part.REPAIR_PART_TYPE.GYRO: return "Gyros"; case Part.REPAIR_PART_TYPE.ACTUATOR: return "Actuators"; case Part.REPAIR_PART_TYPE.ELECTRONICS: return "Cockpit/Life Support/Sensors"; default: return "Other Items"; } } public static String[] findPartImage(IPartWork part) { String imgBase = null; int repairType = IPartWork.findCorrectRepairType(part); switch (repairType) { case Part.REPAIR_PART_TYPE.ARMOR: imgBase = "armor"; break; case Part.REPAIR_PART_TYPE.AMMO: imgBase = "ammo"; break; case Part.REPAIR_PART_TYPE.ACTUATOR: imgBase = "actuator"; break; case Part.REPAIR_PART_TYPE.ENGINE: imgBase = "engine"; break; case Part.REPAIR_PART_TYPE.ELECTRONICS: imgBase = "electronics"; break; case Part.REPAIR_PART_TYPE.HEATSINK: imgBase = "heatsink"; break; case Part.REPAIR_PART_TYPE.WEAPON: EquipmentType equipmentType = null; if (part instanceof EquipmentPart) { equipmentType = ((EquipmentPart)part).getType(); } else if (part instanceof MissingEquipmentPart) { equipmentType = ((MissingEquipmentPart)part).getType(); } if (null != equipmentType) { if (equipmentType.hasFlag(WeaponType.F_LASER)) { imgBase = "laser"; } else if (equipmentType.hasFlag(WeaponType.F_MISSILE)) { imgBase = "missile"; } else if (equipmentType.hasFlag(WeaponType.F_BALLISTIC)) { imgBase = "ballistic"; } else if (equipmentType.hasFlag(WeaponType.F_ARTILLERY)) { imgBase = "artillery"; } } break; case Part.REPAIR_PART_TYPE.MEK_LOCATION: case Part.REPAIR_PART_TYPE.POD_SPACE: imgBase = "location_mek"; break; case Part.REPAIR_PART_TYPE.PHYSICAL_WEAPON: imgBase = "melee"; break; } if (null == imgBase) { imgBase = "equipment"; } String[] imgData = new String[2]; imgData[0] = "data/images/misc/repair/"; imgData[1] = imgBase; return imgData; } }