/* * SpecialAbility.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.personnel; import java.io.FileInputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import megamek.common.EquipmentType; import megamek.common.TechConstants; import megamek.common.WeaponType; import megamek.common.options.IOption; import megamek.common.options.PilotOptions; import megamek.common.weapons.BayWeapon; import megamek.common.weapons.InfantryAttack; import megamek.common.weapons.infantry.InfantryWeapon; import mekhq.MekHQ; import mekhq.MekHqXmlSerializable; import mekhq.MekHqXmlUtil; import mekhq.Utilities; import mekhq.Version; /** * This object will serve as a wrapper for a specific pilot special ability. In the actual * person object we will use PilotOptions (and maybe at some point NonPilotOptions), so these * objects will not get written to actual personnel. Instead, we will we will keep track of a full static * hash of SPAs that will contain important information on XP costs and pre-reqs that can be * looked up to see if a person is eligible for a particular option. All of this * will be customizable via an external XML file that can be user selected in the campaign * options (and possibly user editable). * * @author Jay Lawson <jaylawson39 at yahoo.com> */ public class SpecialAbility implements MekHqXmlSerializable { private static Hashtable<String, SpecialAbility> specialAbilities; private static Hashtable<String, SpecialAbility> defaultSpecialAbilities; private String displayName; private String lookupName; private String desc; private int xpCost; //this determines how much weight to give this SPA when creating new personnel private int weight; //prerequisite skills and options private Vector<String> prereqAbilities; private Vector<SkillPrereq> prereqSkills; //these are abilities that will disqualify the person from getting the current ability private Vector<String> invalidAbilities; //these are abilities that should be removed if the person gets this ability //(typically this is a lower value ability on the same chain (e.g. Cluster Hitter removed when you get Cluster Master) private Vector<String> removeAbilities; public SpecialAbility() { this("unknown"); } public SpecialAbility(String name) { this(name, "", ""); } public SpecialAbility(String name, String display, String description) { lookupName = name; displayName = display; desc = description; prereqAbilities = new Vector<String>(); invalidAbilities = new Vector<String>(); removeAbilities = new Vector<String>(); prereqSkills = new Vector<SkillPrereq>(); xpCost = 1; weight = 1; } @SuppressWarnings("unchecked") // FIXME: Broken Java with it's Object clones public SpecialAbility clone() { SpecialAbility clone = new SpecialAbility(lookupName); clone.displayName = this.displayName; clone.desc = this.desc; clone.xpCost = this.xpCost; clone.weight = this.weight; clone.prereqAbilities = (Vector<String>)this.prereqAbilities.clone(); clone.invalidAbilities = (Vector<String>)this.invalidAbilities.clone(); clone.removeAbilities = (Vector<String>)this.removeAbilities.clone(); clone.prereqSkills = (Vector<SkillPrereq>)this.prereqSkills.clone(); return clone; } public boolean isEligible(Person p) { // Already has this SPA if (p.getSpas() != null && p.getSpas().containsKey(this.getName())) { return false; } // Do we have prerequisite skills? for(SkillPrereq sp : prereqSkills) { if(!sp.qualifies(p)) { return false; } } // Do we have prerequisite abilities? for(String ability : prereqAbilities) { //TODO: will this work for choice options like weapon specialist? if(!p.getOptions().booleanOption(ability)) { return false; } } // Do we have any incompatible abilities? for(String ability : invalidAbilities) { //TODO: will this work for choice options like weapon specialist? if(p.getOptions().booleanOption(ability)) { return false; } } return true; } public String getDisplayName() { return displayName; } public String getDescription() { return desc; } public String getName() { return lookupName; } public int getCost() { return xpCost; } public void setCost(int cost) { xpCost = cost; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } public Vector<SkillPrereq> getPrereqSkills() { return prereqSkills; } public void setPrereqSkills(Vector<SkillPrereq> prereq) { prereqSkills = prereq; } public Vector<String> getPrereqAbilities() { return prereqAbilities; } public void setPrereqAbilities(Vector<String> prereq) { prereqAbilities = prereq; } public Vector<String> getInvalidAbilities() { return invalidAbilities; } public void setInvalidAbilities(Vector<String> invalid) { invalidAbilities = invalid; } public Vector<String> getRemovedAbilities() { return removeAbilities; } public void setRemovedAbilities(Vector<String> remove) { removeAbilities = remove; } public void clearPrereqSkills() { prereqSkills = new Vector<SkillPrereq>(); } @Override public void writeToXml(PrintWriter pw1, int indent) { pw1.println(MekHqXmlUtil.indentStr(indent) + "<ability>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<displayName>" +MekHqXmlUtil.escape(displayName) +"</displayName>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<lookupName>" +lookupName +"</lookupName>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<desc>" +MekHqXmlUtil.escape(desc) +"</desc>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<xpCost>" +xpCost +"</xpCost>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<weight>" +weight +"</weight>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<prereqAbilities>" +Utilities.combineString(prereqAbilities, "::") +"</prereqAbilities>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<invalidAbilities>" +Utilities.combineString(invalidAbilities, "::") +"</invalidAbilities>"); pw1.println(MekHqXmlUtil.indentStr(indent+1) +"<removeAbilities>" +Utilities.combineString(removeAbilities, "::") +"</removeAbilities>"); for(SkillPrereq skillpre : prereqSkills) { skillpre.writeToXml(pw1, indent+1); } pw1.println(MekHqXmlUtil.indentStr(indent) + "</ability>"); } @SuppressWarnings("unchecked") public static void generateInstanceFromXML(Node wn, PilotOptions options, Version v) { SpecialAbility retVal = null; try { retVal = new SpecialAbility(); NodeList nl = wn.getChildNodes(); for (int x=0; x<nl.getLength(); x++) { Node wn2 = nl.item(x); if (wn2.getNodeName().equalsIgnoreCase("displayName")) { retVal.displayName = wn2.getTextContent(); } else if (wn2.getNodeName().equalsIgnoreCase("desc")) { retVal.desc = wn2.getTextContent(); } else if (wn2.getNodeName().equalsIgnoreCase("lookupName")) { retVal.lookupName = wn2.getTextContent(); } else if (wn2.getNodeName().equalsIgnoreCase("xpCost")) { retVal.xpCost = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("weight")) { retVal.weight = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("prereqAbilities")) { retVal.prereqAbilities = Utilities.splitString(wn2.getTextContent(), "::"); } else if (wn2.getNodeName().equalsIgnoreCase("invalidAbilities")) { retVal.invalidAbilities = Utilities.splitString(wn2.getTextContent(), "::"); } else if (wn2.getNodeName().equalsIgnoreCase("removeAbilities")) { retVal.removeAbilities = Utilities.splitString(wn2.getTextContent(), "::"); } else if (wn2.getNodeName().equalsIgnoreCase("skillPrereq")) { SkillPrereq skill = SkillPrereq.generateInstanceFromXML(wn2); if(!skill.isEmpty()) { retVal.prereqSkills.add(skill); } } } } catch (Exception ex) { // Errrr, apparently either the class name was invalid... // Or the listed name doesn't exist. // Doh! MekHQ.logError(ex); } if(retVal.displayName.isEmpty()) { IOption option = options.getOption(retVal.lookupName); if(null != option) { retVal.displayName = option.getDisplayableName(); } } if(retVal.desc.isEmpty()) { IOption option = options.getOption(retVal.lookupName); if(null != option) { retVal.desc = option.getDescription(); } } if (v != null) { if (defaultSpecialAbilities != null && Version.versionCompare(v, "0.3.6-r1965")) { if (defaultSpecialAbilities.get(retVal.lookupName) != null && defaultSpecialAbilities.get(retVal.lookupName).getPrereqSkills() != null) { retVal.prereqSkills = (Vector<SkillPrereq>) defaultSpecialAbilities.get(retVal.lookupName).getPrereqSkills().clone(); } } } specialAbilities.put(retVal.lookupName, retVal); } public static void generateSeparateInstanceFromXML(Node wn, Hashtable<String, SpecialAbility> spHash, PilotOptions options) { SpecialAbility retVal = null; try { retVal = new SpecialAbility(); NodeList nl = wn.getChildNodes(); for (int x=0; x<nl.getLength(); x++) { Node wn2 = nl.item(x); if (wn2.getNodeName().equalsIgnoreCase("displayName")) { retVal.displayName = wn2.getTextContent(); } else if (wn2.getNodeName().equalsIgnoreCase("desc")) { retVal.desc = wn2.getTextContent(); } else if (wn2.getNodeName().equalsIgnoreCase("lookupName")) { retVal.lookupName = wn2.getTextContent(); } else if (wn2.getNodeName().equalsIgnoreCase("xpCost")) { retVal.xpCost = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("weight")) { retVal.weight = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("prereqAbilities")) { retVal.prereqAbilities = Utilities.splitString(wn2.getTextContent(), "::"); } else if (wn2.getNodeName().equalsIgnoreCase("invalidAbilities")) { retVal.invalidAbilities = Utilities.splitString(wn2.getTextContent(), "::"); } else if (wn2.getNodeName().equalsIgnoreCase("removeAbilities")) { retVal.removeAbilities = Utilities.splitString(wn2.getTextContent(), "::"); } else if (wn2.getNodeName().equalsIgnoreCase("skillPrereq")) { SkillPrereq skill = SkillPrereq.generateInstanceFromXML(wn2); if(!skill.isEmpty()) { retVal.prereqSkills.add(skill); } } } } catch (Exception ex) { // Errrr, apparently either the class name was invalid... // Or the listed name doesn't exist. // Doh! MekHQ.logError(ex); } if(retVal.displayName.isEmpty()) { IOption option = options.getOption(retVal.lookupName); if(null != option) { retVal.displayName = option.getDisplayableName(); } } if(retVal.desc.isEmpty()) { IOption option = options.getOption(retVal.lookupName); if(null != option) { retVal.desc = option.getDescription(); } } spHash.put(retVal.lookupName, retVal); } public static void initializeSPA() { specialAbilities = new Hashtable<String, SpecialAbility>(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document xmlDoc = null; try { FileInputStream fis = new FileInputStream("data/universe/defaultspa.xml"); // Using factory get an instance of document builder DocumentBuilder db = dbf.newDocumentBuilder(); // Parse using builder to get DOM representation of the XML file xmlDoc = db.parse(fis); } catch (Exception ex) { MekHQ.logError(ex); } Element spaEle = xmlDoc.getDocumentElement(); NodeList nl = spaEle.getChildNodes(); // Get rid of empty text nodes and adjacent text nodes... // Stupid weird parsing of XML. At least this cleans it up. spaEle.normalize(); PilotOptions options = new PilotOptions(); // Okay, lets iterate through the children, eh? for (int x = 0; x < nl.getLength(); x++) { Node wn = nl.item(x); if (wn.getParentNode() != spaEle) continue; int xc = wn.getNodeType(); if (xc == Node.ELEMENT_NODE) { // This is what we really care about. // All the meat of our document is in this node type, at this // level. // Okay, so what element is it? String xn = wn.getNodeName(); if (xn.equalsIgnoreCase("ability")) { SpecialAbility.generateInstanceFromXML(wn, options, null); } } } SpecialAbility.trackDefaultSPA(); } public static SpecialAbility getAbility(String name) { return specialAbilities.get(name); } public static Hashtable<String, SpecialAbility> getAllSpecialAbilities() { return specialAbilities; } public static SpecialAbility getDefaultAbility(String name) { return defaultSpecialAbilities.get(name); } public static Hashtable<String, SpecialAbility> getAllDefaultSpecialAbilities() { return defaultSpecialAbilities; } public static void replaceSpecialAbilities(Hashtable<String, SpecialAbility> spas) { specialAbilities = spas; } public static String chooseWeaponSpecialization(int type, boolean isClan, int techLvl, int year) { ArrayList<String> candidates = new ArrayList<String>(); for (Enumeration<EquipmentType> e = EquipmentType.getAllTypes(); e.hasMoreElements();) { EquipmentType et = e.nextElement(); if(!(et instanceof WeaponType)) { continue; } if(et instanceof InfantryWeapon || et instanceof BayWeapon || et instanceof InfantryAttack) { continue; } WeaponType wt = (WeaponType)et; if(wt.isCapital() || wt.isSubCapital() || wt.hasFlag(WeaponType.F_INFANTRY) || wt.hasFlag(WeaponType.F_ONESHOT) || wt.hasFlag(WeaponType.F_PROTOTYPE)) { continue; } if(!((wt.hasFlag(WeaponType.F_MECH_WEAPON) && type == Person.T_MECHWARRIOR) || (wt.hasFlag(WeaponType.F_AERO_WEAPON) && type != Person.T_AERO_PILOT) || (wt.hasFlag(WeaponType.F_TANK_WEAPON) && !(type == Person.T_VEE_GUNNER || type == Person.T_NVEE_DRIVER || type == Person.T_GVEE_DRIVER || type == Person.T_VTOL_PILOT)) || (wt.hasFlag(WeaponType.F_BA_WEAPON) && type != Person.T_BA) || (wt.hasFlag(WeaponType.F_PROTO_WEAPON) && type != Person.T_PROTO_PILOT))) { continue; } if(wt.getAtClass() == WeaponType.CLASS_NONE || wt.getAtClass() == WeaponType.CLASS_POINT_DEFENSE || wt.getAtClass() >= WeaponType.CLASS_CAPITAL_LASER) { continue; } if(TechConstants.isClan(wt.getTechLevel(year)) != isClan) { continue; } int lvl = wt.getTechLevel(year); if(lvl < 0) { continue; } if(techLvl < Utilities.getSimpleTechLevel(lvl)) { continue; } if(techLvl == TechConstants.T_IS_UNOFFICIAL) { continue; } int ntimes = 10; if(techLvl >= TechConstants.T_IS_ADVANCED) { ntimes = 1; } while(ntimes > 0) { candidates.add(et.getName()); ntimes--; } } if(candidates.isEmpty()) { return "??"; } return Utilities.getRandomItem(candidates); } public String getAllPrereqDesc() { String toReturn = ""; for(String prereq : prereqAbilities) { toReturn += getDisplayName(prereq) + "<br>"; } for(SkillPrereq skPr : prereqSkills) { toReturn += skPr.toString() + "<br>"; } if(toReturn.isEmpty()) { toReturn = "None"; } return toReturn; } public String getPrereqAbilDesc() { String toReturn = ""; for(String prereq : prereqAbilities) { toReturn += getDisplayName(prereq) + "<br>"; } if(toReturn.isEmpty()) { toReturn = "None"; } return toReturn; } public String getInvalidDesc() { String toReturn = ""; for(String invalid : invalidAbilities) { toReturn += getDisplayName(invalid) + "<br>"; } if(toReturn.isEmpty()) { toReturn = "None"; } return toReturn; } public String getRemovedDesc() { String toReturn = ""; for(String remove : removeAbilities) { toReturn += getDisplayName(remove) + "<br>"; } if(toReturn.isEmpty()) { toReturn = "None"; } return toReturn; } public static String getDisplayName(String name) { PilotOptions options = new PilotOptions(); IOption option = options.getOption(name); if(null != option) { return option.getDisplayableName(); } return "??"; } public static void clearSPA() { specialAbilities.clear(); } @SuppressWarnings("unchecked") public static void trackDefaultSPA() { defaultSpecialAbilities = (Hashtable<String, SpecialAbility>)specialAbilities.clone(); } public static void nullifyDefaultSPA() { defaultSpecialAbilities = null; } public static void setSpecialAbilities(Hashtable<String, SpecialAbility> spHash) { specialAbilities = spHash; } //TODO: also put some static methods here that return the available options for a given SPA, so //we can take that out of the GUI code }