/*
* Armor.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.text.DecimalFormat;
import java.util.GregorianCalendar;
import java.util.SortedSet;
import java.util.TreeSet;
import megamek.common.Aero;
import megamek.common.Entity;
import megamek.common.EquipmentType;
import megamek.common.IArmorState;
import megamek.common.Tank;
import megamek.common.TargetRoll;
import megamek.common.TechConstants;
import mekhq.MekHqXmlUtil;
import mekhq.Utilities;
import mekhq.campaign.Campaign;
import mekhq.campaign.personnel.SkillType;
import mekhq.campaign.work.IAcquisitionWork;
import mekhq.campaign.work.WorkTime;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
*
* @author Jay Lawson <jaylawson39 at yahoo.com>
*/
public class Armor extends Part implements IAcquisitionWork {
private static final long serialVersionUID = 5275226057484468868L;
protected int type;
protected int amount;
protected int amountNeeded;
protected int location;
private boolean rear;
protected boolean clan;
public Armor() {
this(0, 0, 0, -1, false, false, null);
}
public Armor(int tonnage, int t, int points, int loc, boolean r, boolean clan, Campaign c) {
// Amount is used for armor quantity, not tonnage
super(tonnage, c);
this.type = t;
this.amount = points;
this.location = loc;
this.rear = r;
this.clan = clan;
this.name = "Armor";
if(type > -1) {
this.name += " (" + EquipmentType.armorNames[type] + ")";
}
}
public Armor clone() {
Armor clone = new Armor(0, type, amount, -1, false, clan, campaign);
clone.copyBaseData(this);
return clone;
}
@Override
public double getTonnage() {
return amount / getArmorPointsPerTon();
}
@Override
public long getCurrentValue() {
return (long)(getTonnage() * EquipmentType.getArmorCost(type));
}
public double getTonnageNeeded() {
double armorPerTon = 16.0 * EquipmentType.getArmorPointMultiplier(type, isClanTechBase());
if (type == EquipmentType.T_ARMOR_HARDENED) {
armorPerTon = 8.0;
}
return amountNeeded / armorPerTon;
}
public long getValueNeeded() {
return adjustCostsForCampaignOptions((long)(getTonnageNeeded() * EquipmentType.getArmorCost(type)));
}
@Override
public long getStickerPrice() {
//always in 5-ton increments
return (long)(5 * EquipmentType.getArmorCost(type));
}
@Override
public long getBuyCost() {
return getStickerPrice();
}
public String getDesc() {
if(isSalvaging()) {
return super.getDesc();
}
String bonus = getAllMods(null).getValueAsString();
if (getAllMods(null).getValue() > -1) {
bonus = "+" + bonus;
}
bonus = "(" + bonus + ")";
String toReturn = "<html><font size='2'";
String scheduled = "";
if (getTeamId() != null) {
scheduled = " (scheduled) ";
}
toReturn += ">";
toReturn += "<b>Replace " + getName() + "</b><br/>";
toReturn += getDetails() + "<br/>";
if(getAmountAvailable() > 0) {
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;
}
@Override
public String getDetails() {
if(null != unit) {
String rearMount = "";
if(rear) {
rearMount = " (R)";
}
if(!isSalvaging()) {
String availability = "";
int amountAvailable = getAmountAvailable();
if(!isSalvaging()) {
String[] inventories = campaign.getPartInventory(getNewPart());
if(amountAvailable == 0) {
availability = "<br><font color='red'>No armor ("+ inventories[1] + " in transit, " + inventories[2] + " on order)</font>";
} else if(amountAvailable < amountNeeded) {
availability = "<br><font color='red'>Only " + amountAvailable + " available ("+ inventories[1] + " in transit, " + inventories[2] + " on order)</font>";
}
}
return unit.getEntity().getLocationName(location) + rearMount + ", " + amountNeeded + " points" + availability;
}
return unit.getEntity().getLocationName(location) + rearMount + ", " + amount + " points";
}
return amount + " points";
}
public int getType() {
return type;
}
public int getAmount() {
return amount;
}
public int getAmountNeeded() {
return amountNeeded;
}
public int getTotalAmount() {
return amount + amountNeeded;
}
public int getLocation() {
return location;
}
public String getLocationName() {
return unit.getEntity().getLocationName(location);
}
public boolean isRearMounted() {
return rear;
}
public void setAmount(int amount) {
this.amount = amount;
}
public void setAmountNeeded(int needed) {
this.amountNeeded = needed;
}
public boolean isSameType(Armor armor) {
if(getType() == EquipmentType.T_ARMOR_STANDARD
&& armor.getType() == EquipmentType.T_ARMOR_STANDARD) {
//standard armor is compatible between clan and IS
return true;
}
return getType() == armor.getType() && isClanTechBase() == armor.isClanTechBase();
}
@Override
public boolean isSamePartType(Part part) {
return part instanceof Armor
&& isSameType((Armor)part)
&& getRefitId() == part.getRefitId();
}
@Override
public boolean isSameStatus(Part part) {
return this.getDaysToArrival() == part.getDaysToArrival();
}
@Override
public int getIntroDate() {
EquipmentType etype = EquipmentType.get(EquipmentType.getArmorTypeName(type, clan));
if(null == etype) {
return TechConstants.T_TECH_UNKNOWN;
}
return etype.getIntroductionDate();
}
@Override
public int getExtinctDate() {
EquipmentType etype = EquipmentType.get(EquipmentType.getArmorTypeName(type, clan));
if(null == etype) {
return TechConstants.T_TECH_UNKNOWN;
}
return etype.getExtinctionDate();
}
@Override
public int getReIntroDate() {
EquipmentType etype = EquipmentType.get(EquipmentType.getArmorTypeName(type, clan));
if(null == etype) {
return TechConstants.T_TECH_UNKNOWN;
}
return etype.getReintruductionDate();
}
@Override
public int getTechLevel() {
//just use what is already in equipment types to figure it out
EquipmentType etype = EquipmentType.get(EquipmentType.getArmorTypeName(type, clan));
if(null == etype) {
return TechConstants.T_TECH_UNKNOWN;
}
int techLevel = etype.getTechLevel(campaign.getCalendar().get(GregorianCalendar.YEAR));
if(techLevel == TechConstants.T_TECH_UNKNOWN && !etype.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>(etype.getTechLevels().keySet());
techLevel = etype.getTechLevels().get(keys.first());
}
if ((techLevel != TechConstants.T_ALLOWED_ALL && techLevel < 0) || techLevel >= TechConstants.T_ALL)
return TechConstants.T_TECH_UNKNOWN;
else
return techLevel;
}
public double getArmorWeight(int points) {
// from megamek.common.Entity.getArmorWeight()
// this roundabout method is actually necessary to avoid rounding
// weirdness. Yeah, it's dumb.
double armorPointMultiplier = EquipmentType.getArmorPointMultiplier(getType(), isClanTechBase());
double armorPerTon = 16.0 * armorPointMultiplier;
if (getType() == EquipmentType.T_ARMOR_HARDENED) {
armorPerTon = 8.0;
}
double armorWeight = points / armorPerTon;
armorWeight = Math.ceil(armorWeight * 2.0) / 2.0;
return armorWeight;
}
@Override
public void writeToXml(PrintWriter pw1, int indent) {
writeToXmlBegin(pw1, indent);
pw1.println(MekHqXmlUtil.indentStr(indent+1)
+"<amount>"
+amount
+"</amount>");
pw1.println(MekHqXmlUtil.indentStr(indent+1)
+"<type>"
+type
+"</type>");
pw1.println(MekHqXmlUtil.indentStr(indent+1)
+"<location>"
+location
+"</location>");
pw1.println(MekHqXmlUtil.indentStr(indent+1)
+"<rear>"
+rear
+"</rear>");
pw1.println(MekHqXmlUtil.indentStr(indent+1)
+"<amountNeeded>"
+amountNeeded
+"</amountNeeded>");
pw1.println(MekHqXmlUtil.indentStr(indent+1)
+"<clan>"
+clan
+"</clan>");
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("amount")) {
amount = Integer.parseInt(wn2.getTextContent());
} else if (wn2.getNodeName().equalsIgnoreCase("type")) {
type = Integer.parseInt(wn2.getTextContent());
} else if (wn2.getNodeName().equalsIgnoreCase("location")) {
location = Integer.parseInt(wn2.getTextContent());
} else if (wn2.getNodeName().equalsIgnoreCase("amountNeeded")) {
amountNeeded = Integer.parseInt(wn2.getTextContent());
} else if (wn2.getNodeName().equalsIgnoreCase("rear")) {
if(wn2.getTextContent().equalsIgnoreCase("true")) {
rear = true;
} else {
rear = false;
}
} else if (wn2.getNodeName().equalsIgnoreCase("clan")) {
if(wn2.getTextContent().equalsIgnoreCase("true")) {
clan = true;
} else {
clan = false;
}
}
}
}
@Override
public int getAvailability(int era) {
switch(type) {
case EquipmentType.T_ARMOR_FERRO_FIBROUS:
case EquipmentType.T_ARMOR_FERRO_FIBROUS_PROTO:
if(era == EquipmentType.ERA_SL) {
return EquipmentType.RATING_D;
} else if(era == EquipmentType.ERA_SW) {
return EquipmentType.RATING_F;
} else {
return EquipmentType.RATING_D;
}
case EquipmentType.T_ARMOR_LIGHT_FERRO:
case EquipmentType.T_ARMOR_HEAVY_FERRO:
case EquipmentType.T_ARMOR_STEALTH:
if(era == EquipmentType.ERA_SL) {
return EquipmentType.RATING_X;
} else if(era == EquipmentType.ERA_SW) {
return EquipmentType.RATING_X;
} else {
return EquipmentType.RATING_E;
}
case EquipmentType.T_ARMOR_INDUSTRIAL:
if(era == EquipmentType.ERA_SL) {
return EquipmentType.RATING_B;
} else if(era == EquipmentType.ERA_SW) {
return EquipmentType.RATING_C;
} else {
return EquipmentType.RATING_B;
}
case EquipmentType.T_ARMOR_COMMERCIAL:
if(era == EquipmentType.ERA_SL) {
return EquipmentType.RATING_B;
} else if(era == EquipmentType.ERA_SW) {
return EquipmentType.RATING_B;
} else {
return EquipmentType.RATING_A;
}
case EquipmentType.T_ARMOR_REACTIVE:
case EquipmentType.T_ARMOR_REFLECTIVE:
case EquipmentType.T_ARMOR_HARDENED:
case EquipmentType.T_ARMOR_PATCHWORK:
case EquipmentType.T_ARMOR_FERRO_IMP:
case EquipmentType.T_ARMOR_FERRO_CARBIDE:
case EquipmentType.T_ARMOR_LAMELLOR_FERRO_CARBIDE:
case EquipmentType.T_ARMOR_FERRO_LAMELLOR:
if(era == EquipmentType.ERA_SL) {
return EquipmentType.RATING_X;
} else if(era == EquipmentType.ERA_SW) {
return EquipmentType.RATING_X;
} else {
return EquipmentType.RATING_F;
}
default:
return EquipmentType.RATING_C;
}
}
@Override
public int getTechRating() {
EquipmentType etype = EquipmentType.get(EquipmentType.getArmorTypeName(type, clan));
if (null == etype || type == EquipmentType.T_ARMOR_STANDARD) {
return EquipmentType.RATING_D;
}
return etype.getTechRating();
}
@Override
public void fix() {
int amountFound = Math.min(getAmountAvailable(), amountNeeded);
int fixAmount = Math.min(amount + amountFound, unit.getEntity().getOArmor(location, rear));
unit.getEntity().setArmor(fixAmount, location, rear);
changeAmountAvailable(-1 * amountFound);
updateConditionFromEntity(false);
skillMin = SkillType.EXP_GREEN;
shorthandedMod = 0;
}
@Override
public String find(int transitDays) {
Part newPart = getNewPart();
newPart.setBrandNew(true);
newPart.setDaysToArrival(transitDays);
if(campaign.buyPart(newPart, transitDays)) {
return "<font color='green'><b> part found</b>.</font> It will be delivered in " + transitDays + " days.";
} else {
return "<font color='red'><b> You cannot afford this part. Transaction cancelled</b>.</font>";
}
}
@Override
public Object getNewEquipment() {
return getNewPart();
}
@Override
public String failToFind() {
resetDaysToWait();
return "<font color='red'><b> part not found</b>.</font>";
}
@Override
public MissingPart getMissingPart() {
//no such thing
return null;
}
@Override
public IAcquisitionWork getAcquisitionWork() {
return new Armor(0, type, (int)Math.round(5 * getArmorPointsPerTon()), -1, false, clan, campaign);
}
@Override
public void remove(boolean salvage) {
unit.getEntity().setArmor(IArmorState.ARMOR_DESTROYED, location, rear);
if(salvage) {
changeAmountAvailable(amount);
}
updateConditionFromEntity(false);
}
public int getBaseTimeFor(Entity entity) {
if(entity instanceof Tank) {
return 3;
}
else if(entity instanceof Aero) {
return 15;
}
return 5;
}
@Override
public void updateConditionFromEntity(boolean checkForDestruction) {
if(isReservedForRefit()) {
return;
}
if(null == unit) {
return;
}
amount = unit.getEntity().getArmorForReal(location, rear);
if(amount < 0) {
amount = 0;
}
amountNeeded = unit.getEntity().getOArmor(location, rear) - amount;
}
@Override
public int getBaseTime() {
if(isSalvaging()) {
return getBaseTimeFor(unit.getEntity()) * amount;
}
return getBaseTimeFor(unit.getEntity()) * Math.min(amountNeeded, getAmountAvailable());
}
@Override
public int getDifficulty() {
return -2;
}
@Override
public boolean isSalvaging() {
return super.isSalvaging() && amount > 0;
}
@Override
public boolean needsFixing() {
return amountNeeded > 0;
}
@Override
public void updateConditionFromPart() {
if(null != unit) {
int armor = Math.min(amount, unit.getEntity().getOArmor(location, rear));
if(armor == 0) {
armor = IArmorState.ARMOR_DESTROYED;
}
unit.getEntity().setArmor(armor, location, rear);
}
}
@Override
public String checkFixable() {
if(isSalvaging()) {
return null;
}
if(getAmountAvailable() == 0) {
return "No spare armor available";
}
if (isMountedOnDestroyedLocation()) {
return unit.getEntity().getLocationName(location) + " is destroyed.";
}
return null;
}
@Override
public boolean isMountedOnDestroyedLocation() {
return null != unit && unit.isLocationDestroyed(location);
}
@Override
public boolean onBadHipOrShoulder() {
return null != unit && unit.hasBadHipOrShoulder(location);
}
@Override
public String getAcquisitionDesc() {
String toReturn = "<html><font size='2'";
toReturn += ">";
toReturn += "<b>" + getAcquisitionDisplayName() + "</b> " + getAcquisitionBonus() + "<br/>";
toReturn += getAcquisitionExtraDesc() + "<br/>";
String[] inventories = campaign.getPartInventory(getAcquisitionPart());
toReturn += inventories[1] + " in transit, " + inventories[2] + " on order<br>";
toReturn += Utilities.getCurrencyString(adjustCostsForCampaignOptions(getStickerPrice())) + "<br/>";
toReturn += "</font></html>";
return toReturn;
}
@Override
public String getAcquisitionDisplayName() {
return getName();
}
@Override
public String getAcquisitionExtraDesc() {
return ((int)Math.round(getArmorPointsPerTon())) * 5 + " points (5 tons)";
}
@Override
public String getAcquisitionName() {
return getName();
}
@Override
public String getAcquisitionBonus() {
String bonus = getAllAcquisitionMods().getValueAsString();
if(getAllAcquisitionMods().getValue() > -1) {
bonus = "+" + bonus;
}
return "(" + bonus + ")";
}
@Override
public Part getAcquisitionPart() {
return getNewPart();
}
@Override
public TargetRoll getAllAcquisitionMods() {
TargetRoll target = new TargetRoll();
// Faction and Tech mod
if(isClanTechBase() && campaign.getCampaignOptions().getClanAcquisitionPenalty() > 0) {
target.addModifier(campaign.getCampaignOptions().getClanAcquisitionPenalty(), "clan-tech");
}
else if(campaign.getCampaignOptions().getIsAcquisitionPenalty() > 0) {
target.addModifier(campaign.getCampaignOptions().getIsAcquisitionPenalty(), "Inner Sphere tech");
}
//availability mod
int avail = getAvailability(campaign.getEra());
int availabilityMod = Availability.getAvailabilityModifier(avail);
target.addModifier(availabilityMod, "availability (" + EquipmentType.getRatingName(avail) + ")");
return target;
}
public double getArmorPointsPerTon() {
//if(null != unit) {
// armor is checked for in 5-ton increments
//int armorType = unit.getEntity().getArmorType(location);
double armorPerTon = 16.0 * EquipmentType.getArmorPointMultiplier(type, clan);
if (type == EquipmentType.T_ARMOR_HARDENED) {
armorPerTon = 8.0;
}
return armorPerTon;
//}
//return 0.0;
}
public Part getNewPart() {
return new Armor(0, type, (int)Math.round(5 * getArmorPointsPerTon()), -1, false, clan, campaign);
}
public boolean isEnoughSpareArmorAvailable() {
return getAmountAvailable() >= amountNeeded;
}
public int getAmountAvailable() {
for(Part part : campaign.getSpareParts()) {
if(part instanceof Armor) {
Armor a = (Armor)part;
if(isSameType(a) && !a.isReservedForRefit() && a.isPresent()) {
return a.getAmount();
}
}
}
return 0;
}
public void changeAmountAvailable(int amount) {
Armor a = null;
for(Part part : campaign.getSpareParts()) {
if(part instanceof Armor && isSameType((Armor)part)
&& getRefitId() == part.getRefitId()
&& part.isPresent()) {
a = (Armor)part;
a.setAmount(a.getAmount() + amount);
break;
}
}
if(null != a && a.getAmount() <= 0) {
campaign.removePart(a);
} else if(null == a && amount > 0) {
campaign.addPart(new Armor(getUnitTonnage(), type, amount, -1, false, isClanTechBase(), campaign), 0);
}
}
@Override
public String fail(int rating) {
skillMin = ++rating;
timeSpent = 0;
shorthandedMod = 0;
//if we are impossible to fix now, we should scrap this amount of armor
//from spares and start over
String scrap = "";
if(skillMin > SkillType.EXP_ELITE) {
scrap = " Armor supplies lost!";
if(isSalvaging()) {
remove(false);
} else {
skillMin = SkillType.EXP_GREEN;
changeAmountAvailable(-1 * Math.min(amountNeeded, getAmountAvailable()));
}
}
return " <font color='red'><b> failed." + scrap + "</b></font>";
}
@Override
public String scrap() {
remove(false);
skillMin = SkillType.EXP_GREEN;
return EquipmentType.armorNames[type] + " armor scrapped.";
}
@Override
public boolean isInSupply() {
//int currentArmor = Math.max(0, unit.getEntity().getArmorForReal(location, rear));
//int fullArmor = unit.getEntity().getOArmor(location, rear);
//int neededArmor = fullArmor - currentArmor;
return amountNeeded <= getAmountAvailable();
}
@Override
public String getQuantityName(int quan) {
double totalTon = quan * getTonnage();
String report = "" + DecimalFormat.getInstance().format(totalTon) + " tons of " + getName();
if(totalTon == 1.0) {
report = "" + DecimalFormat.getInstance().format(totalTon) + " ton of " + getName();
}
return report;
}
@Override
public String getArrivalReport() {
double totalTon = quantity * getTonnage();
String report = getQuantityName(quantity);
if(totalTon == 1.0) {
report += " has arrived";
} else {
report += " have arrived";
}
return report;
}
public void doMaintenanceDamage(int d) {
int current = unit.getEntity().getArmor(location, rear);
if(d >= current) {
unit.getEntity().setArmor(IArmorState.ARMOR_DESTROYED, location, rear);
} else {
unit.getEntity().setArmor(current - d, location, rear);
}
updateConditionFromEntity(false);
}
@Override
public boolean isPriceAdustedForAmount() {
return true;
}
public void changeType(int ty, boolean cl) {
this.type = ty;
this.clan = cl;
this.name = "Armor";
if(type > -1) {
this.name += " (" + EquipmentType.armorNames[type] + ")";
}
}
@Override
public int getMassRepairOptionType() {
return Part.REPAIR_PART_TYPE.ARMOR;
}
}