/* * AmmoBin.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.Arrays; import java.util.HashSet; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import megamek.common.Aero; import megamek.common.AmmoType; import megamek.common.CriticalSlot; import megamek.common.EquipmentType; import megamek.common.Jumpship; import megamek.common.Mounted; import megamek.common.Protomech; import megamek.common.SmallCraft; import megamek.common.TargetRoll; import mekhq.MekHqXmlUtil; import mekhq.Utilities; import mekhq.campaign.Campaign; import mekhq.campaign.CampaignOptions; import mekhq.campaign.parts.AmmoStorage; import mekhq.campaign.parts.Availability; import mekhq.campaign.parts.MissingPart; import mekhq.campaign.parts.Part; import mekhq.campaign.personnel.Person; import mekhq.campaign.universe.Era; import mekhq.campaign.work.IAcquisitionWork; /** * * @author Jay Lawson <jaylawson39 at yahoo.com> */ public class AmmoBin extends EquipmentPart implements IAcquisitionWork { private static final long serialVersionUID = 2892728320891712304L; public static final Integer[] ALLOWED_BY_TYPE_ARRAY = { AmmoType.T_LRM, AmmoType.T_LRM_PRIMITIVE, AmmoType.T_LRM_STREAK, AmmoType.T_LRM_TORPEDO, AmmoType.T_LRM_TORPEDO_COMBO, AmmoType.T_SRM, AmmoType.T_SRM_ADVANCED, AmmoType.T_SRM_PRIMITIVE, AmmoType.T_SRM_STREAK, AmmoType.T_SRM_TORPEDO, AmmoType.T_MRM, AmmoType.T_MRM_STREAK, AmmoType.T_ROCKET_LAUNCHER, AmmoType.T_EXLRM, AmmoType.T_PXLRM, AmmoType.T_HSRM, AmmoType.T_MML, AmmoType.T_NLRM }; public static final HashSet<Integer> ALLOWED_BY_TYPE = new HashSet<Integer>(Arrays.asList(ALLOWED_BY_TYPE_ARRAY)); protected long munition; protected int shotsNeeded; protected boolean checkedToday; protected boolean oneShot; public AmmoBin() { this(0, null, -1, 0, false, false, null); } public AmmoBin(int tonnage, EquipmentType et, int equipNum, int shots, boolean singleShot, boolean omniPodded, Campaign c) { super(tonnage, et, equipNum, omniPodded, c); this.shotsNeeded = shots; this.oneShot = singleShot; this.checkedToday = false; if(null != type && type instanceof AmmoType) { this.munition = ((AmmoType)type).getMunitionType(); } if(null != name) { this.name += " Bin"; } } public AmmoBin clone() { AmmoBin clone = new AmmoBin(getUnitTonnage(), getType(), getEquipmentNum(), shotsNeeded, oneShot, omniPodded, campaign); clone.copyBaseData(this); clone.shotsNeeded = this.shotsNeeded; clone.munition = this.munition; return clone; } /* Per TM, ammo for fighters is stored in the fuselage. This makes a difference for omnifighter * pod space, so we're going to stick them in LOC_NONE where the heat sinks are */ @Override public String getLocationName() { if (unit.getEntity() instanceof Aero && !((unit.getEntity() instanceof SmallCraft) || (unit.getEntity() instanceof Jumpship))){ return "Fuselage"; } return super.getLocationName(); } @Override public int getLocation() { if (unit.getEntity() instanceof Aero && !((unit.getEntity() instanceof SmallCraft) || (unit.getEntity() instanceof Jumpship))){ return Aero.LOC_NONE; } return super.getLocation(); } @Override public boolean isInLocation(String loc) { if (unit.getEntity() instanceof Aero && !((unit.getEntity() instanceof SmallCraft) || (unit.getEntity() instanceof Jumpship))){ return loc.equals("FSLG"); } return super.isInLocation(loc); } @Override public double getTonnage() { return (1.0 * getFullShots())/((AmmoType)type).getShots(); } public int getFullShots() { int fullShots = ((AmmoType)type).getShots(); if(unit != null) { Mounted m = unit.getEntity().getEquipment(equipmentNum); if(null != m && m.getOriginalShots() > 0) { fullShots = m.getOriginalShots(); } } if(null != unit && unit.getEntity() instanceof Protomech) { //if protomechs are using alternate munitions then cut in half if(((AmmoType)type).getMunitionType() != AmmoType.M_STANDARD) { fullShots = fullShots / 2; } } if(oneShot) { fullShots = 1; } return fullShots; } protected int getCurrentShots() { int shots = getFullShots() - shotsNeeded; //replace with actual entity values if entity not null because the previous number will not //be correct for ammo swaps if(null != unit && null != unit.getEntity()) { Mounted m = unit.getEntity().getEquipment(equipmentNum); if(null != m) { shots = m.getBaseShotsLeft(); } } return shots; } public long getValueNeeded() { return adjustCostsForCampaignOptions((long)(getPricePerTon() * ((double)shotsNeeded / getShotsPerTon()))); } protected long getPricePerTon() { //if on a unit, then use the ammo type on the existing entity, to avoid getting it wrong due to //ammo swaps EquipmentType curType = type; if(null != unit && null != unit.getEntity()) { Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted && (mounted.getType() instanceof AmmoType)) { curType = mounted.getType(); } } return (long)curType.getRawCost(); } protected int getShotsPerTon() { AmmoType atype = (AmmoType)type; if(atype.getKgPerShot() > 0) { return (int)Math.floor(1000.0/atype.getKgPerShot()); } //if not listed by kg per shot, we assume this is a single ton increment return ((AmmoType)type).getShots(); } @Override public long getStickerPrice() { return (long)(getPricePerTon() * (1.0 * getCurrentShots()/getShotsPerTon())); } @Override public long getBuyCost() { return getNewPart().getStickerPrice(); } public int getShotsNeeded() { return shotsNeeded; } public void changeMunition(long m) { this.munition = m; for (AmmoType atype : Utilities.getMunitionsFor(unit.getEntity(),(AmmoType)type, CampaignOptions.TECH_EXPERIMENTAL)) { if (atype.getMunitionType() == munition) { type = atype; break; } } updateConditionFromEntity(false); } @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) +"<munition>" +munition +"</munition>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<shotsNeeded>" +shotsNeeded +"</shotsNeeded>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<checkedToday>" +checkedToday +"</checkedToday>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<oneShot>" +oneShot +"</oneShot>"); writeToXmlEnd(pw1, indent); } @Override protected void loadFieldsFromXmlNode(Node wn) { super.loadFieldsFromXmlNode(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("munition")) { munition = Long.parseLong(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("shotsNeeded")) { shotsNeeded = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("checkedToday")) { if(wn2.getTextContent().equalsIgnoreCase("true")) { checkedToday = true; } else { checkedToday = false; } } else if (wn2.getNodeName().equalsIgnoreCase("oneShot")) { if(wn2.getTextContent().equalsIgnoreCase("true")) { oneShot = true; } else { oneShot = false; } } } restore(); } public void restoreMunitionType() { for (AmmoType atype : Utilities.getMunitionsFor(unit.getEntity(),(AmmoType)type, CampaignOptions.TECH_EXPERIMENTAL)) { if (atype.getMunitionType() == munition && atype.getInternalName().equals(type.getInternalName())) { type = atype; break; } } } public long getMunitionType() { return munition; } @Override public int getAvailability(int era) { return type.getAvailability(Era.convertEra(era)); } @Override public int getTechRating() { return type.getTechRating(); } /* @Override public int getTechBase() { return T_BOTH; } */ @Override public String getStatus() { String toReturn = "Fully Loaded"; if(shotsNeeded >= getFullShots()) { toReturn = "Empty"; } else if (shotsNeeded > 0) { toReturn = "Partially Loaded"; } if(isReservedForRefit()) { toReturn += " (Reserved for Refit)"; } return toReturn; } @Override public void fix() { loadBin(); } public void loadBin() { int shots = Math.min(getAmountAvailable(), shotsNeeded); if(null != unit) { Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted && mounted.getType() instanceof AmmoType) { if(mounted.getType().equals(type) && ((AmmoType)mounted.getType()).getMunitionType() == getMunitionType()) { //just a simple reload mounted.setShotsLeft(mounted.getBaseShotsLeft() + shots); } else { //loading a new type of ammo unload(); mounted.changeAmmoType((AmmoType)type); mounted.setShotsLeft(shots); } } } changeAmountAvailable(-1 * shots, (AmmoType)type); shotsNeeded -= shots; } public void setShotsNeeded(int shots) { this.shotsNeeded = shots; } @Override public String find(int transitDays) { return "<font color='red'> You shouldn't be here (AmmoBin.find()).</font>"; } @Override public String failToFind() { return "<font color='red'> You shouldn't be here (AmmoBin.failToFind()).</font>"; } public void unload() { //FIXME: the following won't work for proto and Dropper bins if they //are not attached to a unit. Currently the only place AmmoBins are loaded //off of units is for refits, which neither of those units can do, but we //may want to think about not having refits load ammo bins but rather reserve //some AmmoStorage instead if we implement customization of these units int shots = getFullShots() - shotsNeeded; AmmoType curType = (AmmoType)type; if(null != unit) { Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted && mounted.getType() instanceof AmmoType) { shots = mounted.getBaseShotsLeft(); mounted.setShotsLeft(0); curType = (AmmoType)mounted.getType(); } } shotsNeeded = getFullShots(); if(shots > 0) { changeAmountAvailable(shots, curType); } } @Override public void remove(boolean salvage) { if(salvage) { unload(); } super.remove(salvage); } @Override public MissingPart getMissingPart() { return new MissingAmmoBin(getUnitTonnage(), type, equipmentNum, oneShot, omniPodded, campaign); } public boolean isOneShot() { return oneShot; } @Override public TargetRoll getAllMods(Person tech) { if(isSalvaging()) { return super.getAllMods(tech); } return new TargetRoll(TargetRoll.AUTOMATIC_SUCCESS, "ammo loading"); } @Override public void updateConditionFromEntity(boolean checkForDestruction) { if(null != unit) { Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted) { if(mounted.isMissing() || mounted.isDestroyed()) { mounted.setShotsLeft(0); remove(false); return; } long currentMuniType = 0; if(mounted.getType() instanceof AmmoType) { currentMuniType = ((AmmoType)mounted.getType()).getMunitionType(); } if(currentMuniType == getMunitionType()) { shotsNeeded = getFullShots() - mounted.getBaseShotsLeft(); } else { //we have a change of munitions shotsNeeded = getFullShots(); } } } } @Override public int getBaseTime() { if(isSalvaging()) { return isOmniPodded()? 30 : 120; } if(null != unit) { Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted) { long currentMuniType = 0; if(mounted.getType() instanceof AmmoType) { currentMuniType = ((AmmoType)mounted.getType()).getMunitionType(); } if(getMunitionType() != currentMuniType) { return 30; } } } return 15; } @Override public int getActualTime() { if (isOmniPodded()) { return (int)Math.ceil(getBaseTime() * mode.timeMultiplier * 0.5); } return (int) Math.ceil(getBaseTime() * mode.timeMultiplier); } @Override public int getDifficulty() { if(isSalvaging()) { return -2; } return 0; } @Override public void updateConditionFromPart() { if(null != unit) { Mounted mounted = unit.getEntity().getEquipment(equipmentNum); if(null != mounted) { mounted.setHit(false); mounted.setDestroyed(false); mounted.setRepairable(true); mounted.changeAmmoType((AmmoType) type); unit.repairSystem(CriticalSlot.TYPE_EQUIPMENT, equipmentNum); mounted.setShotsLeft(getFullShots() - shotsNeeded); } } } @Override public boolean isSamePartType(Part part) { return part instanceof AmmoBin && getType().equals( ((AmmoBin)part).getType() ) && ((AmmoBin)part).getFullShots() == getFullShots(); } @Override public boolean needsFixing() { return shotsNeeded > 0;// && null != unit; } public String getDesc() { if(isSalvaging()) { return super.getDesc(); } String toReturn = "<html><font size='2'"; String scheduled = ""; if (getTeamId() != null) { scheduled = " (scheduled) "; } toReturn += ">"; toReturn += "<b>Reload " + getName() + "</b><br/>"; toReturn += getDetails() + "<br/>"; toReturn += "" + getTimeLeft() + " minutes" + scheduled; toReturn += "</font></html>"; return toReturn; } @Override public String getDetails() { if(isSalvaging()) { return super.getDetails(); } if(null != unit) { String availability = ""; int shotsAvailable = getAmountAvailable(); String[] inventories = campaign.getPartInventory(getNewPart()); if(shotsAvailable == 0) { availability = "<br><font color='red'>No ammo ("+ inventories[1] + " in transit, " + inventories[2] + " on order)</font>"; } else if(shotsAvailable < shotsNeeded) { availability = "<br><font color='red'>Only " + shotsAvailable + " available ("+ inventories[1] + " in transit, " + inventories[2] + " on order)</font>"; } return ((AmmoType)type).getDesc() + ", " + shotsNeeded + " shots needed" + availability; } else { return ""; } } @Override public String checkFixable() { if(!isSalvaging() && getAmountAvailable() == 0) { return "No ammo of this type is available"; } if(null == unit) { return "Ammo bins can only be loaded when installed on units"; } return null; } public void swapAmmoFromCompatible(int needed, AmmoStorage as) { AmmoStorage a = null; AmmoType aType = null; AmmoType curType = ((AmmoType)((AmmoStorage)as).getType()); int converted = 0; for(Part part : campaign.getSpareParts()) { if(!part.isPresent()) { continue; } if(part instanceof AmmoStorage) { a = (AmmoStorage)part; aType = ((AmmoType)a.getType()); if (a.isSamePartType(as)) { continue; } if (!isCompatibleAmmo(aType, curType)) { continue; } if (a.getShots() == 0) { continue; } // Finally, do the conversion. Run until the other ammo type runs out or we have enough converted = aType.getRackSize(); a.changeAmountAvailable(-1, aType); while (converted < needed && a.getShots() > 0) { converted += aType.getRackSize(); a.changeAmountAvailable(-1, aType); } needed -= converted; // If we have enough, we're done. if (converted >= needed) { break; } } } converted = Math.round((float)converted/curType.getRackSize()); as.changeAmountAvailable(converted, curType); } public void changeAmountAvailable(int amount, AmmoType curType) { AmmoStorage a = null; AmmoType aType = null; for(Part part : campaign.getSpareParts()) { if(!part.isPresent()) { continue; } if(part instanceof AmmoStorage) { aType = ((AmmoType)((AmmoStorage)part).getType()); if(aType.equals(curType) && curType.getMunitionType() == aType.getMunitionType()) { a = (AmmoStorage)part; if (amount < 0 && campaign.getCampaignOptions().useAmmoByType() && a.getShots() < Math.abs(amount)) { swapAmmoFromCompatible(Math.abs(amount) * aType.getRackSize(), a); } a.changeShots(amount); break; } } } if(null != a && a.getShots() <= 0) { campaign.removePart(a); } else if(null == a && amount > 0) { campaign.addPart(new AmmoStorage(1, curType, amount, campaign), 0); } else if (a == null && amount < 0 && campaign.getCampaignOptions().useAmmoByType() && AmmoBin.ALLOWED_BY_TYPE.contains(curType)) { campaign.addPart(new AmmoStorage(1 , curType ,0, campaign), 0); changeAmountAvailable(amount, curType); } } public boolean isCompatibleAmmo(AmmoType a1, AmmoType a2) { // If the option isn't on, we don't use this! if (!(campaign.getCampaignOptions().useAmmoByType())) { return false; } // NPE protection if (a1 == null || a2 == null) { return false; } // If it isn't an allowed type, then nope! if (!AmmoBin.ALLOWED_BY_TYPE.contains(a1.getAmmoType()) || !AmmoBin.ALLOWED_BY_TYPE.contains(a2.getAmmoType())) { return false; } // Now we begin to compare boolean result = false; // MML Launchers, ugh. if ((a1.getAmmoType() == AmmoType.T_MML || a2.getAmmoType() == AmmoType.T_MML) && a1.getMunitionType() == a2.getMunitionType()) { // LRMs... if (a1.getAmmoType() == AmmoType.T_MML && a1.hasFlag(AmmoType.F_MML_LRM) && a2.getAmmoType() == AmmoType.T_LRM) { result = true; } else if (a2.getAmmoType() == AmmoType.T_MML && a2.hasFlag(AmmoType.F_MML_LRM) && a1.getAmmoType() == AmmoType.T_LRM) { result = true; } // SRMs if (a1.getAmmoType() == AmmoType.T_MML && !a1.hasFlag(AmmoType.F_MML_LRM) && a2.getAmmoType() == AmmoType.T_SRM) { result = true; } else if (a2.getAmmoType() == AmmoType.T_MML && !a2.hasFlag(AmmoType.F_MML_LRM) && a1.getAmmoType() == AmmoType.T_SRM) { result = true; } } // AR-10 Launchers, ugh. /*if (a1.getAmmoType() == AmmoType.T_AR10 || a2.getAmmoType() == AmmoType.T_AR10) { // Barracuda if (a1.getAmmoType() == AmmoType.T_AR10 && a1.hasFlag(AmmoType.F_AR10_BARRACUDA) && a2.getAmmoType() == AmmoType.T_BARRACUDA) { result = true; } else if (a2.getAmmoType() == AmmoType.T_AR10 && a2.hasFlag(AmmoType.F_AR10_BARRACUDA) && a1.getAmmoType() == AmmoType.T_BARRACUDA) { result = true; } // Killer Whale if (a1.getAmmoType() == AmmoType.T_AR10 && a1.hasFlag(AmmoType.F_AR10_KILLER_WHALE) && a2.getAmmoType() == AmmoType.T_KILLER_WHALE) { result = true; } else if (a2.getAmmoType() == AmmoType.T_AR10 && a2.hasFlag(AmmoType.F_AR10_KILLER_WHALE) && a1.getAmmoType() == AmmoType.T_KILLER_WHALE) { result = true; } // White Shark if (a1.getAmmoType() == AmmoType.T_AR10 && a1.hasFlag(AmmoType.F_AR10_WHITE_SHARK) && a2.getAmmoType() == AmmoType.T_WHITE_SHARK) { result = true; } else if (a2.getAmmoType() == AmmoType.T_AR10 && a2.hasFlag(AmmoType.F_AR10_WHITE_SHARK) && a1.getAmmoType() == AmmoType.T_WHITE_SHARK) { result = true; } }*/ // General Launchers if (a1.getAmmoType() == a2.getAmmoType() && a1.getMunitionType() == a2.getMunitionType()) { result = true; } return result; } public int getAmountAvailable() { int amount = 0; AmmoStorage a = null; AmmoType aType = null; AmmoType thisType = null; for(Part part : campaign.getSpareParts()) { if(!part.isPresent()) { continue; } if(part instanceof AmmoStorage) { a = (AmmoStorage)part; aType = ((AmmoType)((AmmoStorage)a).getType()); thisType = ((AmmoType)getType()); if(aType.equals((Object)getType()) && thisType.getMunitionType() == aType.getMunitionType()) { amount += a.getShots(); if (!(campaign.getCampaignOptions().useAmmoByType())) { break; } } else if (isCompatibleAmmo(aType, thisType) && thisType.getRackSize() != 0) { amount += a.getShots()*aType.getRackSize()/thisType.getRackSize(); } } } return amount; } public boolean isEnoughSpareAmmoAvailable() { return getAmountAvailable() >= shotsNeeded; } @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(getBuyCost()) + "<br/>"; toReturn += "</font></html>"; return toReturn; } @Override public String getAcquisitionDisplayName() { return type.getDesc(); } @Override public String getAcquisitionExtraDesc() { return ((AmmoType)type).getShots() + " shots (1 ton)"; } @Override public String getAcquisitionBonus() { String bonus = getAllAcquisitionMods().getValueAsString(); if(getAllAcquisitionMods().getValue() > -1) { bonus = "+" + bonus; } return "(" + bonus + ")"; } @Override public Part getAcquisitionPart() { return getNewPart(); } @Override public String getAcquisitionName() { return type.getDesc(); } @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; } @Override public Object getNewEquipment() { return getNewPart(); } public Part getNewPart() { return new AmmoStorage(1,type,shotsNeeded,campaign); } @Override public IAcquisitionWork getAcquisitionWork() { int shots = 1; if(type instanceof AmmoType) { shots = ((AmmoType)type).getShots(); } return new AmmoStorage(1,type,shots,campaign); } @Override public boolean needsMaintenance() { return false; } @Override public boolean isPriceAdustedForAmount() { return true; } @Override public int getMassRepairOptionType() { return Part.REPAIR_PART_TYPE.AMMO; } @Override public boolean isOmniPoddable() { return true; } /** * Since ammo bins aren't real parts they can't be podded in the warehouse, and * whether they're podded on the unit depends entirely on the unit they're installed on. */ @Override public boolean isOmniPodded() { return getUnit() != null && getUnit().getEntity().getEquipment(equipmentNum).isOmniPodMounted(); } }