/*
* RetirementDefectionTracker.java
*
* Copyright (c) 2014 Carl Spain. 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.PrintWriter;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import megamek.common.Compute;
import megamek.common.TargetRoll;
import megamek.common.options.IOption;
import megamek.common.options.PilotOptions;
import mekhq.MekHQ;
import mekhq.MekHqXmlSerializable;
import mekhq.MekHqXmlUtil;
import mekhq.Utilities;
import mekhq.campaign.Campaign;
import mekhq.campaign.mission.AtBContract;
import mekhq.campaign.mission.Mission;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* @author Neoancient
*
* Against the Bot
* Utility class that handles retirement/defection rolls and final payments
* to personnel who retire/defect/get sacked and families of those killed
* in battle.
*
*/
public class RetirementDefectionTracker implements Serializable, MekHqXmlSerializable{
/**
*
*/
private static final long serialVersionUID = 7245317499458320654L;
/* In case the dialog is closed after making the retirement rolls
* and determining payouts but before the retirees have been paid,
* we store those results to avoid making the rolls again.
*/
private HashSet<Integer> rollRequired;
private HashMap<Integer, HashSet<UUID>> unresolvedPersonnel;
private HashMap<UUID, Payout> payouts;
private GregorianCalendar lastRetirementRoll;
public RetirementDefectionTracker() {
rollRequired = new HashSet<Integer>();
unresolvedPersonnel = new HashMap<Integer, HashSet<UUID>>();
payouts = new HashMap<UUID, Payout>();
lastRetirementRoll = new GregorianCalendar();
}
/**
*
* @param campaign
* @return The value of each share in C-bills
*/
public static long getShareValue(Campaign campaign) {
if (!campaign.getCampaignOptions().getUseShareSystem()) {
return 0;
}
String financialReport = campaign.getFinancialReport();
long netWorth = 0;
try {
DecimalFormat df = new DecimalFormat();
Pattern p = Pattern.compile("Net Worth\\D*(.*)");
Matcher m = p.matcher(financialReport);
m.find();
netWorth = (Long)(df.parse(m.group(1)));
if (campaign.getCampaignOptions().getSharesExcludeLargeCraft()) {
p = Pattern.compile("Large Craft\\D*(.*)");
m = p.matcher(financialReport);
if (m.find() && null != m.group(1)) {
netWorth -= (Long)(df.parse(m.group(1)));
}
}
} catch (Exception e) {
MekHQ.logError("Error parsing net worth in financial report");
MekHQ.logError(e);
}
int totalShares = 0;
for (Person p : campaign.getPersonnel()) {
totalShares += p.getNumShares(campaign.getCampaignOptions().getSharesForAll());
}
return netWorth / totalShares;
}
/**
*
* Computes the target for retirement rolls for all eligible personnel; this includes
* all active personnel who are not dependents, prisoners, or bondsmen.
*
* @param contract The contract that is being resolved; if the retirement roll is not
* due to contract resolutions (e.g. > 12 months since last roll), this
* can be null.
* @param campaign
* @return A map with person ids as key and calculated target roll as value.
*/
public HashMap<UUID, TargetRoll> calculateTargetNumbers(AtBContract contract,
Campaign campaign) {
HashMap <UUID, TargetRoll> targets = new HashMap<UUID, TargetRoll>();
int combatLeadershipMod = 0;
int supportLeadershipMod = 0;
if (null != contract) {
rollRequired.add(contract.getId());
}
if (campaign.getCampaignOptions().getUseLeadership()) {
int combat = 0;
int proto = 0;
int support = 0;
for (Person p : campaign.getPersonnel()) {
if (!p.isActive() || p.getPrimaryRole() == Person.T_NONE ||
p.isDependent() || p.isPrisoner() || p.isBondsman()) {
continue;
}
if (p.getPrimaryRole() >= Person.T_MECH_TECH) {
support++;
} else if (null == p.getUnitId() ||
(null != campaign.getUnit(p.getUnitId()) && campaign.getUnit(p.getUnitId()).isCommander(p))) {
/* The AtB rules do not state that crews count as a
* single person for leadership purposes, but to do otherwise
* would tax all but the most exceptional commanders of
* vehicle or infantry units.
*/
if (p.getPrimaryRole() == Person.T_PROTO_PILOT) {
proto++;
} else {
combat++;
}
}
}
combat += proto / 5;
int max = 12;
if (null != campaign.getFlaggedCommander() &&
null != campaign.getFlaggedCommander().getSkill(SkillType.S_LEADER)) {
max += 6 * campaign.getFlaggedCommander().getSkill(SkillType.S_LEADER).getLevel();
}
if (combat > 2 * max) {
combatLeadershipMod = 2;
} else if (combat > max) {
combatLeadershipMod = 1;
}
if (support > 2 * max) {
supportLeadershipMod = 2;
} else if (support > max) {
supportLeadershipMod = 1;
}
}
for (Person p : campaign.getPersonnel()) {
if (!p.isActive() || p.isDependent()
|| p.isPrisoner() || p.isBondsman()
|| p.isDeployed()) {
continue;
}
/* Infantry units retire or defect by platoon */
if (null != p.getUnitId() && null != campaign.getUnit(p.getUnitId()) && campaign.getUnit(p.getUnitId()).usesSoldiers() &&
!campaign.getUnit(p.getUnitId()).isCommander(p)) {
continue;
}
TargetRoll target = new TargetRoll(5, "Target");
target.addModifier(p.getExperienceLevel(false) - campaign.getUnitRatingMod(),
"Experience");
/* Retirement rolls are made before the contract status is set */
if (null != contract && (
contract.getStatus() == Mission.S_FAILED ||
contract.getStatus() == Mission.S_BREACH)) {
target.addModifier(1, "Failed mission");
}
if (campaign.getCampaignOptions().getTrackUnitFatigue()
&& campaign.getFatigueLevel() >= 10) {
target.addModifier(campaign.getFatigueLevel() / 10, "Fatigue");
}
if (campaign.getFactionCode().equals("PIR")) {
target.addModifier(1, "Pirate");
}
if (p.getRank().isOfficer()) {
target.addModifier(-1, "Officer");
} else {
for (Enumeration<IOption> i = p.getOptions(PilotOptions.LVL3_ADVANTAGES); i.hasMoreElements(); ) {
IOption ability = i.nextElement();
if (ability.booleanValue()) {
if (ability.getName().equals("tactical_genius")) {
target.addModifier(1, "Non-officer tactical genius");
break;
}
}
}
}
if (p.getAge(campaign.getCalendar()) >= 50) {
target.addModifier(1, "Over 50");
}
if (campaign.getCampaignOptions().getUseShareSystem()) {
/* If this retirement roll is not being made at the end
* of a contract (e.g. >12 months since last roll), the
* share percentage should still apply. In the case of multiple
* active contracts, pick the one with the best percentage.
*/
AtBContract c = contract;
if (null == c) {
for (Mission m : campaign.getMissions()) {
if (m.isActive() && m instanceof AtBContract &&
(null == c || c.getSharesPct() < ((AtBContract)m).getSharesPct())) {
c = (AtBContract)m;
}
}
}
if (null != c && c.getSharesPct() > 20) {
target.addModifier(-((c.getSharesPct() - 20) / 10), "Shares");
}
} else {
//Bonus payments handled by dialog
}
if (p.getPrimaryRole() == Person.T_INFANTRY) {
target.addModifier(-1, "Infantry");
}
int injuryMod = 0;
for (Injury i : p.getInjuries()) {
if (i.isPermanent()) {
injuryMod++;
}
}
if (injuryMod > 0) {
target.addModifier(injuryMod, "Permanent injuries");
}
if (combatLeadershipMod != 0 && p.getPrimaryRole() < Person.T_MECH_TECH) {
target.addModifier(combatLeadershipMod, "Leadership");
}
if (supportLeadershipMod != 0 && p.getPrimaryRole() >= Person.T_MECH_TECH) {
target.addModifier(supportLeadershipMod, "Leadership");
}
targets.put(p.getId(), target);
}
return targets;
}
/**
* Makes rolls for retirement/defection based on previously calculated target rolls,
* and tracks all retirees in the unresolvedPersonnel hash in case the dialog
* is closed before payments are resolved, to avoid rerolling the results.
*
* @param contract
* @param targets The hash previously generated by calculateTargetNumbers.
* @param shareValue The value of each share in the unit; if not using the share system, this is zero.
* @param campaign
*/
public void rollRetirement(AtBContract contract,
HashMap<UUID, TargetRoll> targets, long shareValue, Campaign campaign) {
if (null != contract && !unresolvedPersonnel.keySet().contains(contract.getId())) {
unresolvedPersonnel.put(contract.getId(), new HashSet<UUID>());
}
for (UUID id : targets.keySet()) {
if (Compute.d6(2) < targets.get(id).getValue()) {
if (null != contract) {
unresolvedPersonnel.get(contract.getId()).add(id);
}
payouts.put(id, new Payout(campaign.getPerson(id),
shareValue, false, campaign.getCampaignOptions().getSharesForAll()));
}
}
if (null != contract) {
rollRequired.remove(contract.getId());
}
lastRetirementRoll.setTime(campaign.getDate());
}
public GregorianCalendar getLastRetirementRoll() {
return lastRetirementRoll;
}
public void setLastRetirementRoll(GregorianCalendar cal) {
lastRetirementRoll.setTime(cal.getTime());
}
/**
* Handles final payout to any personnel who are sacked or killed in battle
*
* @param person The person to be removed from the campaign
* @param killed True if killed in battle, false if sacked
* @param shares The number of shares controlled by person
* @param campaign
* @param contract If not null, the payout must be resolved before the
* contract can be resolved.
* @return true if the person is due a payout; otherwise false
*/
public boolean removeFromCampaign(Person person, boolean killed,
int shares, Campaign campaign, AtBContract contract) {
/* Payouts to Infantry/Battle armor platoons/squads/points are
* handled as a unit in the AtB rules, so we're just going to ignore
* them here.
*/
if (person.getPrimaryRole() == Person.T_INFANTRY ||
person.getPrimaryRole() == Person.T_BA ||
person.isPrisoner() || person.isBondsman()) {
return false;
}
payouts.put(person.getId(), new Payout(person, getShareValue(campaign),
killed, campaign.getCampaignOptions().getSharesForAll()));
if (null != contract) {
if (null == unresolvedPersonnel.get(contract.getId())) {
unresolvedPersonnel.put(contract.getId(), new HashSet<UUID>());
}
unresolvedPersonnel.get(contract.getId()).add(person.getId());
}
return true;
}
public void removePayout(Person person) {
payouts.remove(person.getId());
}
public boolean isOutstanding(AtBContract contract) {
return isOutstanding(contract.getId());
}
public boolean isOutstanding(int id) {
return unresolvedPersonnel.keySet().contains(id);
}
/* Called by when all payouts have been resolved for the contract.
* If contract is null, the dialog has been invoked without a
* specific contract and all outstanding payouts have been resolved.
*/
public void resolveAllContracts() {
resolveContract(null);
payouts.clear();
}
public void resolveContract(AtBContract contract) {
if (null == contract) {
for (int id : unresolvedPersonnel.keySet()) {
resolveContract(id);
}
} else {
resolveContract(contract.getId());
}
}
public void resolveContract(int contractId) {
if (null != unresolvedPersonnel.get(contractId)) {
for (UUID pid : unresolvedPersonnel.get(contractId)) {
payouts.remove(pid);
}
unresolvedPersonnel.remove(contractId);
}
rollRequired.remove(contractId);
}
public Set<UUID> getRetirees() {
return getRetirees(null);
}
public Set<UUID> getRetirees(AtBContract contract) {
if (null != contract) {
return unresolvedPersonnel.get(contract.getId());
} else {
return payouts.keySet();
}
}
public Payout getPayout(UUID id) {
return payouts.get(id);
}
/**
*
* @param person
* @return The amount in C-bills required to get a bonus to the retirement/defection roll
*/
public static long getBonusCost(Person person) {
switch (person.getExperienceLevel(false)) {
case SkillType.EXP_ELITE:
return (person.getProfession() == Ranks.RPROF_MW)?300000:150000;
case SkillType.EXP_VETERAN:
return (person.getProfession() == Ranks.RPROF_MW)?150000:50000;
case SkillType.EXP_REGULAR:
return (person.getProfession() == Ranks.RPROF_MW)?50000:20000;
case SkillType.EXP_GREEN:
default:
return (person.getProfession() == Ranks.RPROF_MW)?20000:10000;
}
}
/**
*
* Class used to record the required payout to each retired/defected/killed/sacked
* person.
*
*/
public class Payout {
int weightClass = 0;
int dependents = 0;
long cbills = 0;
boolean recruit = false;
int recruitType = Person.T_NONE;
boolean heir = false;
boolean stolenUnit = false;
UUID stolenUnitId = null;
public Payout() {}
public Payout(Person p, long shareValue, boolean killed, boolean sharesForAll) {
calculatePayout(p, killed, shareValue > 0);
if (shareValue > 0) {
cbills += shareValue * p.getNumShares(sharesForAll);
}
if (killed) {
switch (Compute.d6()) {
case 1:
/* No effects */
break;
case 2:
dependents = 1;
break;
case 3:
dependents = Compute.d6();
break;
case 4:
case 5:
recruit = true;
break;
case 6:
heir = true;
break;
}
}
}
private void calculatePayout(Person p, boolean killed,
boolean shareSystem) {
int roll;
if (killed) {
roll = Utilities.dice(1, 5);
} else {
roll = Compute.d6() + Math.max(-1, p.getExperienceLevel(false) - 2);
if (p.getRank().isOfficer()) {
roll += 1;
}
}
if (roll >= 6 && (p.getPrimaryRole() == Person.T_AERO_PILOT ||
p.getPrimaryRole() == Person.T_AERO_PILOT)) {
stolenUnit = true;
} else {
if (p.getProfession() == Ranks.RPROF_INF) {
if (p.getUnitId() != null) {
cbills = 50000;
}
} else {
cbills = getBonusCost(p);
if (p.getRank().isOfficer()) {
cbills *= 2;
}
}
if (!shareSystem &&
(p.getProfession() == Ranks.RPROF_MW ||
p.getProfession() == Ranks.RPROF_ASF) &&
p.getOriginalUnitWeight() > 0) {
weightClass = p.getOriginalUnitWeight() +
p.getOriginalUnitTech();
if (roll <= 1) {
weightClass--;
}
if (roll >= 5) {
weightClass++;
}
}
}
}
public int getWeightClass() {
return weightClass;
}
public void setWeightClass(int weight) {
weightClass = weight;
}
public int getDependents() {
return dependents;
}
public void setDependents(int d) {
dependents = d;
}
public long getCbills() {
return cbills;
}
public void setCbills(long cbills) {
this.cbills = cbills;
}
public boolean hasRecruit() {
return recruit;
}
public void setRecruit(boolean r) {
recruit = r;
}
public int getRecruitType() {
return recruitType;
}
public void setRecruitType(int type) {
recruitType = type;
}
public boolean hasHeir() {
return heir;
}
public void setHeir(boolean h) {
heir = h;
}
public boolean hasStolenUnit() {
return stolenUnit;
}
public void setStolenUnit(boolean stolen) {
stolenUnit = stolen;
}
public UUID getStolenUnitId() {
return stolenUnitId;
}
public void setStolenUnitId(UUID id) {
stolenUnitId = id;
}
}
private String createCsv(Collection<? extends Object> coll) {
String retVal = "";
if (coll.size() > 0) {
for (Object o : coll) {
retVal += o.toString() + ",";
}
return retVal.substring(0, retVal.length() - 1);
}
return "";
}
@Override
public void writeToXml(PrintWriter pw1, int indent) {
pw1.println(MekHqXmlUtil.indentStr(indent) + "<retirementDefectionTracker>");
MekHqXmlUtil.writeSimpleXmlTag(pw1, indent + 1,
"rollRequired",
createCsv(rollRequired));
pw1.println(MekHqXmlUtil.indentStr(indent + 1)
+ "<unresolvedPersonnel>");
for (Integer i : unresolvedPersonnel.keySet()) {
pw1.println(MekHqXmlUtil.indentStr(indent + 2)
+ "<contract id=\"" + i + "\">"
+ createCsv(unresolvedPersonnel.get(i))
+ "</contract>");
}
pw1.println(MekHqXmlUtil.indentStr(indent + 1)
+ "</unresolvedPersonnel>");
pw1.println(MekHqXmlUtil.indentStr(indent + 1)
+ "<payouts>");
for (UUID pid : payouts.keySet()) {
pw1.println(MekHqXmlUtil.indentStr(indent + 2)
+ "<payout id=\"" + pid.toString() + "\">");
MekHqXmlUtil.writeSimpleXmlTag(pw1, indent + 3,
"weightClass", payouts.get(pid).getWeightClass());
MekHqXmlUtil.writeSimpleXmlTag(pw1, indent + 3,
"dependents", payouts.get(pid).getDependents());
MekHqXmlUtil.writeSimpleXmlTag(pw1, indent + 3,
"cbills", payouts.get(pid).getCbills());
MekHqXmlUtil.writeSimpleXmlTag(pw1, indent + 3,
"recruit", payouts.get(pid).hasRecruit());
MekHqXmlUtil.writeSimpleXmlTag(pw1, indent + 3,
"heir", payouts.get(pid).hasHeir());
MekHqXmlUtil.writeSimpleXmlTag(pw1, indent + 3,
"stolenUnit", payouts.get(pid).hasStolenUnit());
if (null != payouts.get(pid).getStolenUnitId()) {
MekHqXmlUtil.writeSimpleXmlTag(pw1, indent + 3,
"stolenUnitId", payouts.get(pid).getStolenUnitId().toString());
}
pw1.println(MekHqXmlUtil.indentStr(indent + 2)
+ "</payout>");
}
pw1.println(MekHqXmlUtil.indentStr(indent + 1)
+ "</payouts>");
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
MekHqXmlUtil.writeSimpleXmlTag(pw1, indent + 1,
"lastRetirementRoll", df.format(lastRetirementRoll.getTime()));
pw1.println(MekHqXmlUtil.indentStr(indent) + "</retirementDefectionTracker>");
}
public static RetirementDefectionTracker generateInstanceFromXML(Node wn, Campaign c) {
RetirementDefectionTracker retVal = null;
try {
// Instantiate the correct child class, and call its parsing function.
retVal = new RetirementDefectionTracker();
// Okay, now load Part-specific fields!
NodeList nl = wn.getChildNodes();
// Loop through the nodes and load our contract offers
for (int x = 0; x < nl.getLength(); x++) {
Node wn2 = nl.item(x);
// If it's not an element node, we ignore it.
if (wn2.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
if (wn2.getNodeName().equalsIgnoreCase("rollRequired")) {
if (wn2.getTextContent().trim().length() > 0) {
String [] ids = wn2.getTextContent().split(",");
for (String id : ids) {
retVal.rollRequired.add(Integer.parseInt(id));
}
}
} else if (wn2.getNodeName().equalsIgnoreCase("unresolvedPersonnel")) {
NodeList nl2 = wn2.getChildNodes();
for (int y = 0; y < nl2.getLength(); y++) {
Node wn3 = nl2.item(y);
if (wn3.getNodeType() != Node.ELEMENT_NODE){
continue;
}
if (wn3.getNodeName().equalsIgnoreCase("contract")) {
int id = Integer.parseInt(wn3.getAttributes().getNamedItem("id").getTextContent());
HashSet<UUID> pids = new HashSet<UUID>();
String [] ids = wn3.getTextContent().split(",");
for (String s : ids) {
pids.add(UUID.fromString(s));
}
retVal.unresolvedPersonnel.put(id, pids);
}
}
} else if (wn2.getNodeName().equalsIgnoreCase("payouts")) {
NodeList nl2 = wn2.getChildNodes();
for (int y = 0; y < nl2.getLength(); y++) {
Node wn3 = nl2.item(y);
if (wn3.getNodeType() != Node.ELEMENT_NODE){
continue;
}
if (wn3.getNodeName().equalsIgnoreCase("payout")) {
UUID pid = UUID.fromString(wn3.getAttributes().getNamedItem("id").getTextContent());
Payout payout = retVal.new Payout();
NodeList nl3 = wn3.getChildNodes();
for (int z = 0; z < nl3.getLength(); z++) {
Node wn4 = nl3.item(z);
if (wn4.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
if (wn4.getNodeName().equalsIgnoreCase("weightClass")) {
payout.setWeightClass(Integer.parseInt(wn4.getTextContent()));
} else if (wn4.getNodeName().equalsIgnoreCase("dependents")) {
payout.setDependents(Integer.parseInt(wn4.getTextContent()));
} else if (wn4.getNodeName().equalsIgnoreCase("cbills")) {
payout.setCbills(Long.parseLong(wn4.getTextContent()));
} else if (wn4.getNodeName().equalsIgnoreCase("recruit")) {
payout.setRecruit(Boolean.parseBoolean(wn4.getTextContent()));
} else if (wn4.getNodeName().equalsIgnoreCase("heir")) {
payout.setHeir(Boolean.parseBoolean(wn4.getTextContent()));
} else if (wn4.getNodeName().equalsIgnoreCase("stolenUnit")) {
payout.setStolenUnit(Boolean.parseBoolean(wn4.getTextContent()));
} else if (wn4.getNodeName().equalsIgnoreCase("stolenUnitId")) {
payout.setStolenUnitId(UUID.fromString(wn4.getTextContent()));
}
}
retVal.payouts.put(pid, payout);
}
}
} else if (wn2.getNodeName().equalsIgnoreCase("lastRetirementRoll")) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
retVal.lastRetirementRoll.setTime(df.parse(wn2.getTextContent().trim()));
}
}
} catch (Exception ex) {
// Errrr, apparently either the class name was invalid...
// Or the listed name doesn't exist.
// Doh!
MekHQ.logError(ex);
}
return retVal;
}
}