/*
* Bloodname.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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import megamek.common.Compute;
import mekhq.MekHQ;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* @author Neoancient
*
*
*/
public class Bloodname implements Serializable {
/**
*
*/
private static final long serialVersionUID = 3958964485520416824L;
private static ArrayList<Bloodname> bloodnames;
public static final int P_GENERAL = 0;
public static final int P_MECHWARRIOR = 1;
public static final int P_AEROSPACE = 2;
public static final int P_ELEMENTAL = 3;
public static final int P_PROTOMECH = 4;
public static final int P_NAVAL = 5;
public static final int P_NUM = 6;
public static final String[] phenotypeNames = {
"General", "MechWarrior", "Aerospace Pilot", "Elemental",
"ProtoMech Pilot", "Naval Commander"
};
private String name;
private String founder;
private Clan origClan;
private boolean exclusive;
private boolean limited;
private int inactive;
private int abjured;
private int reactivated;
private int startDate;
private int phenotype;
ArrayList<Clan> postReavingClans;
ArrayList<NameAcquired> acquiringClans;
NameAcquired absorbed;
public Bloodname() {
name = "";
founder = "";
exclusive = false;
limited = false;
inactive = 0;
abjured = 0;
reactivated = 0;
startDate = 2807;
phenotype = P_GENERAL;
postReavingClans = new ArrayList<Clan>();
acquiringClans = new ArrayList<NameAcquired>();
absorbed = null;
}
public String getName() {
return name;
}
public String getFounder() {
return founder;
}
public String getOrigClan() {
return origClan.getCode();
}
public boolean isExclusive() {
return exclusive;
}
public boolean isLimited() {
return limited;
}
public boolean isInactive(int year) {
return year < startDate || (inactive > 0 && inactive < year &&
!(reactivated > 0 && reactivated <= year));
}
public boolean isAbjured(int year) {
return abjured > 0 && abjured < year;
}
public int getPhenotype() {
return phenotype;
}
public ArrayList<Clan> getPostReavingClans() {
return postReavingClans;
}
public ArrayList<NameAcquired> getAcquiringClans() {
return acquiringClans;
}
public NameAcquired getAbsorbed() {
return absorbed;
}
/**
*
* @param warriorType A Person.PHENOTYPE_* constant
* @param year The current year of the campaign setting
* @return An adjustment to the frequency of this name for the phenotype.
*
* A warrior is three times as likely to have a Bloodname associated with the
* same phenotype as a general name (which is split among the three types).
* Elemental names are treated as general prior to 2870. The names that later
* became associated with ProtoMech pilots (identified in WoR) are assumed
* to have been poor performers and have a lower frequency even before the
* invention of the PM, though have a higher frequency for PM pilots than other
* aerospace names.
*/
public int phenotypeMultiplier(int warriorType, int year) {
switch (phenotype) {
case P_MECHWARRIOR:
return (warriorType == P_MECHWARRIOR)?3:0;
case P_AEROSPACE:
return (warriorType == P_AEROSPACE || warriorType == P_PROTOMECH)?3:0;
case P_ELEMENTAL:
if (year < 2870) {
return 1;
}
return (warriorType == P_ELEMENTAL)?3:0;
case P_PROTOMECH:
switch (warriorType) {
case P_PROTOMECH:return 9;
case P_AEROSPACE:return 1;
default:return 0;
}
case P_NAVAL:
return (warriorType == P_NAVAL)?3:0;
case P_GENERAL:
default:
return 1;
}
}
public static Bloodname loadFromXml(Node node) {
Bloodname retVal = new Bloodname();
NodeList nl = node.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node wn = nl.item(i);
if (wn.getNodeName().equalsIgnoreCase("name")) {
retVal.name = wn.getTextContent().trim();
} else if (wn.getNodeName().equalsIgnoreCase("founder")) {
retVal.founder = wn.getTextContent().trim();
} else if (wn.getNodeName().equalsIgnoreCase("clan")) {
retVal.origClan = Clan.getClan(wn.getTextContent().trim());
} else if (wn.getNodeName().equalsIgnoreCase("exclusive")) {
retVal.exclusive = true;
} else if (wn.getNodeName().equalsIgnoreCase("reaved")) {
retVal.inactive = Integer.parseInt(wn.getTextContent().trim());
} else if (wn.getNodeName().equalsIgnoreCase("dormant")) {
retVal.inactive = Integer.parseInt(wn.getTextContent().trim()) + 10;
} else if (wn.getNodeName().equalsIgnoreCase("abjured")) {
retVal.abjured = Integer.parseInt(wn.getTextContent().trim());
} else if (wn.getNodeName().equalsIgnoreCase("reactivated")) {
retVal.reactivated = Integer.parseInt(wn.getTextContent().trim() + 20);
} else if (wn.getNodeName().equalsIgnoreCase("phenotype")) {
switch (wn.getTextContent().trim()) {
case "General":
retVal.phenotype = P_GENERAL;
break;
case "MechWarrior":
retVal.phenotype = P_MECHWARRIOR;
break;
case "Aerospace":
retVal.phenotype = P_AEROSPACE;
break;
case "Elemental":
retVal.phenotype = P_ELEMENTAL;
break;
case "ProtoMech":
retVal.phenotype = P_PROTOMECH;
break;
case "Naval":
retVal.phenotype = P_NAVAL;
break;
default:
System.err.println("Unknown phenotype " +
wn.getTextContent() + " in " + retVal.name);
}
} else if (wn.getNodeName().equalsIgnoreCase("postReaving")) {
String[] clans = wn.getTextContent().trim().split(",");
for (String c : clans) {
retVal.postReavingClans.add(Clan.getClan(c));
}
} else if (wn.getNodeName().equalsIgnoreCase("acquired")) {
retVal.acquiringClans.add(retVal.new NameAcquired(
Integer.parseInt(wn.getAttributes().getNamedItem("date").getTextContent()) + 10,
wn.getTextContent().trim()));
} else if (wn.getNodeName().equalsIgnoreCase("shared")) {
retVal.acquiringClans.add(retVal.new NameAcquired(
Integer.parseInt(wn.getAttributes().getNamedItem("date").getTextContent()),
wn.getTextContent().trim()));
} else if (wn.getNodeName().equalsIgnoreCase("absorbed")) {
retVal.absorbed = retVal.new NameAcquired(
Integer.parseInt(wn.getAttributes().getNamedItem("date").getTextContent()),
wn.getTextContent().trim());
} else if (wn.getNodeName().equalsIgnoreCase("created")) {
retVal.startDate = Integer.parseInt(wn.getTextContent().trim()) + 20;
}
}
return retVal;
}
public static Bloodname randomBloodname(String factionCode, int phenotype, int year) {
return randomBloodname(Clan.getClan(factionCode), phenotype, year);
}
/**
* Determines a likely Bloodname based on Clan, phenotype, and year.
*
* @param faction The faction code for the Clan; must exist in data/names/bloodnames/clans.xml
* @param phenotype One of the Person.PHENOTYPE_* constants
* @param year The current campaign year
* @return An object representing the chosen Bloodname
*
* Though based as much as possible on official sources, the method employed here involves a
* considerable amount of speculation.
*/
public static Bloodname randomBloodname(Clan faction, int phenotype, int year) {
if (null == faction) {
MekHQ.logError("Random Bloodname attempted for a clan that does not exist."
+ System.lineSeparator()
+ "Please ensure that your clan exists in both the clans.xml and bloodnames.xml files as appropriate.");
return null;
}
if (Compute.randomInt(20) == 0) {
/* 1 in 20 chance that warrior was taken as isorla from another Clan */
return randomBloodname(faction.getRivalClan(year), phenotype, year);
}
if (Compute.randomInt(20) == 0) {
/* Bloodnames that are predominantly used for a particular phenotype are not
* exclusively used for that phenotype. A 5% chance of ignoring phenotype will
* result in a very small chance (around 1%) of a Bloodname usually associated
* with a different phenotype.
*/
phenotype = Bloodname.P_GENERAL;
}
/* The relative probability of the various Bloodnames that are original to this Clan */
HashMap<Bloodname, Fraction> weights = new HashMap<Bloodname, Fraction>();
/* A list of non-exclusive Bloodnames from other Clans */
ArrayList<Bloodname> nonExclusives = new ArrayList<Bloodname>();
/* The relative probability that a warrior in this Clan will have a non-exclusive
* Bloodname that originally belonged to another Clan; the smaller the number
* of exclusive Bloodnames of this Clan, the larger this chance.
*/
double nonExclusivesWeight = 0.0;
for (Bloodname name : bloodnames) {
/* Bloodnames exclusive to Clans that have been abjured (NC, WIE) continue
* to be used by those Clans but not by others.
*/
if (name.isInactive(year) ||
(name.isAbjured(year) && !name.getOrigClan().equals(faction)) ||
0 == name.phenotypeMultiplier(phenotype, year)) {
continue;
}
Fraction weight = null;
/* Effects of the Wars of Reaving would take a generation to show up
* in the breeding programs, so the tables given in the WoR sourcebook
* are in effect from about 3100 on.
*/
if (year < 3100) {
int numClans = 1;
for (Bloodname.NameAcquired a : name.getAcquiringClans()) {
if (a.year < year) {
numClans++;
}
}
/* Non-exclusive names have a weight of 1 (equal to exclusives) up to 2900,
* then decline 10% per 50 years to a minimum of 0.6 in 3050+. In the few
* cases where the other Clans using the name are known, the weight is
* 1/(number of Clans) instead.
*/
if (name.getOrigClan().equals(faction.getCode()) ||
(null != name.getAbsorbed() && faction.equals(name.getAbsorbed().clan) &&
name.getAbsorbed().year > year)) {
if (name.isExclusive() || numClans > 1) {
weight = new Fraction(1, numClans);
} else {
weight = eraFraction(year);
nonExclusivesWeight += 1 - eraFraction(year).value();
/* The fraction is squared to represent the combined effect
* of increasing distribution among the Clans and the likelihood
* that non-exclusive names would suffer
* more reavings and have a lower Bloodcount.
*/
weight.mul(eraFraction(year));
}
} else {
/* Most non-exclusives have an unknown distribution and are estimated.
* When the actual Clans sharing the Bloodname are known, it is divided
* among those Clans.
*/
for (Bloodname.NameAcquired a : name.getAcquiringClans()) {
if (faction.equals(a.clan)) {
weight = new Fraction(1, numClans);
break;
}
}
if (null == weight && !name.isExclusive()) {
for (int i = 0; i < name.phenotypeMultiplier(phenotype, year); i++) {
nonExclusives.add(name);
}
}
}
} else {
if (name.getPostReavingClans().contains(faction)) {
weight = new Fraction(name.phenotypeMultiplier(phenotype, year),
name.getPostReavingClans().size());
/* Assume that Bloodnames that were exclusive before the Wars of Reaving
* are more numerous (higher bloodcount).
*/
if (!name.isLimited()) {
if (name.isExclusive()) {
weight.mul(4);
} else {
weight.mul(2);
}
}
} else if (name.getPostReavingClans().size() == 0) {
for (int i = 0; i < name.phenotypeMultiplier(phenotype, year); i++) {
nonExclusives.add(name);
}
}
}
if (null != weight) {
weight.mul(name.phenotypeMultiplier(phenotype, year));
weights.put(name, weight);
}
}
int lcd = Fraction.lcd(weights.values());
for (Fraction f : weights.values()) {
f.mul(lcd);
}
ArrayList<Bloodname> nameList = new ArrayList<Bloodname>();
for (Bloodname b : weights.keySet()) {
for (int i = 0; i < weights.get(b).value(); i++) {
nameList.add(b);
}
}
nonExclusivesWeight *= lcd;
if (year >= 3100) {
nonExclusivesWeight = nameList.size() / 10.0;
}
int roll = Compute.randomInt(nameList.size() + (int)(nonExclusivesWeight + 0.5));
if (roll > nameList.size() - 1) {
return nonExclusives.get(Compute.randomInt(nonExclusives.size()));
}
return nameList.get(roll);
}
/**
* Represents the decreasing frequency of non-exclusive names within the original Clan
* due to dispersal throughout the Clans and reavings.
*
* @param year The current year of the campaign
* @return A fraction that decreases by 10%/year
*/
private static Fraction eraFraction(int year) {
if (year < 2900) {
return new Fraction(1);
}
if (year < 2950) {
return new Fraction(9, 10);
}
if (year < 3000) {
return new Fraction(4, 5);
}
if (year < 3050) {
return new Fraction(7, 10);
}
return new Fraction (3, 5);
}
public static void loadBloodnameData() {
Clan.loadClanData();
bloodnames = new ArrayList<Bloodname>();
File f = new File("data/names/bloodnames/bloodnames.xml");
FileInputStream fis = null;
try {
fis = new FileInputStream(f);
} catch (FileNotFoundException e) {
MekHQ.logError("Cannot find file bloodnames.xml");
return;
}
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Document doc = null;
try {
DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.parse(fis);
} catch (Exception ex) {
System.err.println(ex.getMessage());
}
Element bloodnameElement = doc.getDocumentElement();
NodeList nl = bloodnameElement.getChildNodes();
bloodnameElement.normalize();
for (int i = 0; i < nl.getLength(); i++) {
Node wn = nl.item(i);
if (wn.getNodeType() == Node.ELEMENT_NODE) {
if (wn.getNodeName().equalsIgnoreCase("bloodname")) {
bloodnames.add(Bloodname.loadFromXml(wn));
}
}
}
MekHQ.logMessage("Loaded " + bloodnames.size() + " Bloodname records.");
}
public class NameAcquired {
public int year;
public String clan;
public NameAcquired(int y, String c) {
year = y;
clan = c;
}
}
}
class DatedRecord {
public int startDate;
public int endDate;
public String descr;
public DatedRecord(int s, int e, String d) {
startDate = s;
endDate = e;
descr = d;
}
}
class Clan {
private static HashMap<String, Clan> allClans;
private String code;
private String fullName;
private int startDate;
private int endDate;
private int abjurationDate;
private ArrayList<DatedRecord> rivals;
private ArrayList<DatedRecord> nameChanges;
private boolean homeClan;
public Clan() {
startDate = endDate = abjurationDate = 0;
rivals = new ArrayList<DatedRecord>();
nameChanges = new ArrayList<DatedRecord>();
}
@Override
public boolean equals(Object o) {
if (o instanceof Clan) {
return code.equals(((Clan)o).code);
}
if (o instanceof String) {
return code.equals((String)o);
}
return false;
}
public static Clan getClan(String code) {
return allClans.get(code);
}
public String getCode() {
return code;
}
public String getFullName(int year) {
for (DatedRecord r : nameChanges) {
if (r.startDate < year &&
(r.endDate == 0 || r.endDate > year)) {
return r.descr;
}
}
return fullName;
}
public boolean isActive(int year) {
return startDate < year && (endDate == 0 || endDate > year);
}
public boolean isAbjured(int year) {
if (abjurationDate == 0) return false;
return abjurationDate < year;
}
public ArrayList<Clan> getRivals(int year) {
ArrayList<Clan> retVal = new ArrayList<Clan>();
for (DatedRecord r : rivals) {
if (r.startDate < year && (endDate == 0) || endDate > year) {
Clan c = allClans.get(r.descr);
if (c.isActive(year)) {
retVal.add(c);
}
}
}
return retVal;
}
public boolean isHomeClan() {
return homeClan;
}
public Clan getRivalClan(int year) {
ArrayList<Clan> rivals = getRivals(year);
int roll = Compute.randomInt(rivals.size() + 1);
if (roll > rivals.size() - 1) {
return randomClan(year, homeClan);
}
return rivals.get(roll);
}
public static Clan randomClan(int year, boolean homeClan) {
ArrayList<Clan> list = new ArrayList<Clan>();
for (Clan c : allClans.values()) {
if (year > 3075 && homeClan != c.homeClan) {
continue;
}
if (c.isActive(year)) {
list.add(c);
}
}
return list.get(Compute.randomInt(list.size()));
}
public static void loadClanData() {
allClans = new HashMap<String, Clan>();
File f = new File("data/names/bloodnames/clans.xml");
FileInputStream fis = null;
try {
fis = new FileInputStream(f);
} catch (FileNotFoundException e) {
MekHQ.logError("Cannot find file clans.xml");
return;
}
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Document doc = null;
try {
DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.parse(fis);
} catch (Exception ex) {
System.err.println(ex.getMessage());
}
Element clanElement = doc.getDocumentElement();
NodeList nl = clanElement.getChildNodes();
clanElement.normalize();
for (int i = 0; i < nl.getLength(); i++) {
Node wn = nl.item(i);
if (wn.getNodeName().equalsIgnoreCase("clan")) {
Clan c = loadFromXml(wn);
allClans.put(c.code, c);
}
}
}
private static Clan loadFromXml(Node node) {
Clan retVal = new Clan();
retVal.code = node.getAttributes().getNamedItem("code").getTextContent().trim();
if (null != node.getAttributes().getNamedItem("start")) {
retVal.startDate = Integer.parseInt(node.getAttributes().getNamedItem("start").getTextContent().trim());
}
if (null != node.getAttributes().getNamedItem("end")) {
retVal.endDate = Integer.parseInt(node.getAttributes().getNamedItem("end").getTextContent().trim());
}
NodeList nl = node.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node wn = nl.item(i);
if (wn.getNodeName().equalsIgnoreCase("fullName")) {
retVal.fullName = wn.getTextContent().trim();
} else if (wn.getNodeName().equalsIgnoreCase("abjured")) {
retVal.abjurationDate = Integer.parseInt(wn.getTextContent().trim());
} else if (wn.getNodeName().equalsIgnoreCase("nameChange")) {
int start = retVal.startDate;
int end = retVal.endDate;
if (null != wn.getAttributes().getNamedItem("start")) {
start = Integer.parseInt(wn.getAttributes().getNamedItem("start").getTextContent().trim());
}
if (null != wn.getAttributes().getNamedItem("end")) {
end = Integer.parseInt(wn.getAttributes().getNamedItem("end").getTextContent().trim());
}
retVal.nameChanges.add(new DatedRecord(start, end, wn.getTextContent().trim()));
} else if (wn.getNodeName().equalsIgnoreCase("rivals")) {
int start = retVal.startDate;
int end = retVal.endDate;
if (null != wn.getAttributes().getNamedItem("start")) {
start = Integer.parseInt(wn.getAttributes().getNamedItem("start").getTextContent().trim());
}
if (null != wn.getAttributes().getNamedItem("end")) {
end = Integer.parseInt(wn.getAttributes().getNamedItem("end").getTextContent().trim());
}
String[] rivals = wn.getTextContent().trim().split(",");
for (String r : rivals) {
retVal.rivals.add(new DatedRecord(start, end, r));
}
} else if (wn.getNodeName().equalsIgnoreCase("homeClan")) {
retVal.homeClan = true;
}
}
return retVal;
}
}
class Fraction {
private int numerator;
private int denominator;
public Fraction() {
numerator = 0;
denominator = 1;
}
public Fraction(int n, int d) {
if (d == 0) {
throw new IllegalArgumentException("Denominator is zero.");
}
if (d < 0) {
n = -n;
d = -d;
}
numerator = n;
denominator = d;
}
public Fraction(int i) {
numerator = i;
denominator = 1;
}
public Fraction(Fraction f) {
numerator = f.numerator;
denominator = f.denominator;
}
@Override
public Object clone() {
return new Fraction(this);
}
@Override
public String toString() {
return numerator + "/" + denominator;
}
public boolean equals(Fraction f) {
return value() == f.value();
}
public double value() {
return (double)numerator / (double)denominator;
}
public void reduce() {
if (denominator > 1) {
for (int i = denominator - 1; i > 1; i--) {
if (numerator % i == 0 && denominator % i == 0) {
numerator /= i;
denominator /= i;
i = denominator - 1;
}
}
}
}
public int getNumerator() {
return numerator;
}
public int getDenominator() {
return denominator;
}
public void add(Fraction f) {
numerator = numerator * f.denominator + f.numerator * denominator;
denominator = denominator * f.denominator;
reduce();
}
public void add(int i) {
numerator += i * denominator;
reduce();
}
public void sub(Fraction f) {
numerator = numerator * f.denominator - f.numerator * denominator;
denominator = denominator * f.denominator;
reduce();
}
public void sub(int i) {
numerator -= i * denominator;
reduce();
}
public void mul(Fraction f) {
numerator *= f.numerator;
denominator *= f.denominator;
reduce();
}
public void mul(int i) {
numerator *= i;
reduce();
}
public void div(Fraction f) {
numerator *= f.denominator;
denominator *= f.numerator;
reduce();
}
public void div(int i) {
denominator *= i;
}
public static int lcd(Collection<Fraction> list) {
HashSet<Integer> denominators = new HashSet<Integer>();
for (Fraction f : list) {
denominators.add(f.denominator);
}
boolean done = false;
int retVal = 1;
while (!done) {
done = true;
for (Integer d : denominators) {
if (d / retVal > 1 || retVal % d != 0) {
retVal++;
done = false;
break;
}
}
}
return retVal;
}
}