/*
* BioSet.java
* Copyright 2002 (C) Bryan McRoberts <merton_monk@yahoo.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Created on September 27, 2002, 5:30 PM
*
* Current Ver: $Revision$
*
*/
package pcgen.core;
import java.net.URI;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.TreeSet;
import pcgen.base.util.CaseInsensitiveMap;
import pcgen.base.util.DoubleKeyMap;
import pcgen.base.util.TripleKeyMapToList;
import pcgen.cdom.base.Constants;
import pcgen.cdom.base.NonInteractive;
import pcgen.cdom.enumeration.NumericPCAttribute;
import pcgen.cdom.enumeration.PCAttribute;
import pcgen.cdom.enumeration.Region;
import pcgen.util.Logging;
/**
* {@code BioSet}.
*
* @author Bryan McRoberts
*/
public final class BioSet extends PObject implements NonInteractive
{
private DoubleKeyMap<Region, Integer, AgeSet> ageMap = new DoubleKeyMap<>();
private CaseInsensitiveMap<Integer> ageNames = new CaseInsensitiveMap<>();
private TripleKeyMapToList<Region, String, String, String> userMap = new TripleKeyMapToList<>();
public AgeSet getAgeSet(Region region, int index)
{
AgeSet ageSet = ageMap.get(region, index);
if ((ageSet == null) || !ageSet.hasBonuses())
{
ageSet = ageMap.get(Region.getConstant("None"), index);
}
return ageSet;
}
/**
* Get the Age Set named
* @param ageCategory
* @return age set named
*/
public int getAgeSetNamed(final String ageCategory)
{
Integer cat = ageNames.get(ageCategory);
return (cat == null) ? -1 : cat;
}
/**
* Builds a string describing the bio settings for the specified race
* This string is formatted so that it can be read in by BioSetLoader
*
* @param region The region of the race to be output
* @param race The name of the race to be output
* @return String A lst string describing the region's biosets.
*/
public String getRacePCCText(final String region, final String race)
{
final StringBuilder sb = new StringBuilder(1000);
sb.append("REGION:").append(region).append("\n\n");
Region r = Region.getConstant(region);
final SortedMap<Integer, SortedMap<String, SortedMap<String, String>>> ageSets = getRaceTagsByAge(r, race);
return appendAgesetInfo(r, ageSets, sb);
}
/**
* Get the tag for the race
* @param region
* @param race
* @param tag
* @return List
*/
public List<String> getTagForRace(final String region, final String race, final String tag)
{
return getValueInMaps(region, race, tag);
}
/**
* Add the supplied line to the user map. The user map contains an array with
* an entry for each age set. The supplied index is used to ensure that the
* value is placed in the correct age bracket.
*
* @param regionString The region the race is defined in.
* @param race The race to be updated.
* @param tag The tag to be entered. Must be in the form key:value
* @param ageSetIndex The age set to be updated.
*/
public void addToUserMap(String regionString, final String race, final String tag, final int ageSetIndex)
{
final int x = tag.indexOf(':');
if (x < 0)
{
Logging.errorPrint("Invalid value sent to map: " + tag + " (for Race " + race + ")");
return; // invalid tag
}
Region region = Region.getConstant(regionString);
String key = tag.substring(0, x);
List<String> r = userMap.getListFor(region, race, key);
for (int i = (r == null) ? 0 : r.size(); i < ageSetIndex; i++)
{
userMap.addToListFor(region, race, key, "0");
}
userMap.addToListFor(region, race, key, tag.substring(x + 1));
}
/**
* Clear the user map
*/
public void clearUserMap()
{
userMap.clear();
}
/**
* Copies the bio data for one race to a new race.
*
* @param origRegion The region of the original race
* @param origRace The name of the original race
* @param copyRegion The region of the target race
* @param copyRace The name of the target race
*/
public void copyRaceTags(final String origRegion, final String origRace, final String copyRegion, final String copyRace)
{
Region oldr = Region.getConstant(origRegion);
Region newr = Region.getConstant(copyRegion);
for (String key : userMap.getTertiaryKeySet(oldr, origRace))
{
userMap.addAllToListFor(newr, copyRace, key, userMap.getListFor(oldr, origRace, key));
}
final int idx = origRace.indexOf('(');
String otherRace;
if (idx >= 0)
{
otherRace = origRace.substring(0, idx).trim() + '%';
}
else
{
otherRace = origRace + '%';
}
for (String key : userMap.getTertiaryKeySet(oldr, otherRace))
{
userMap.addAllToListFor(newr, copyRace, key, userMap.getListFor(oldr, otherRace, key));
}
}
/**
* Randomizes the values of the passed in attributes.
*
* @param randomizeStr .-delimited list of attributes to randomize. (AGE.HT.WT.EYES.HAIR.SKIN are the possible values.)
* @param pc The Player Character
*/
public void randomize(final String randomizeStr, final PlayerCharacter pc)
{
if ((pc == null) || (pc.getRace() == null))
{
return;
}
final List<String> ranList = new ArrayList<>();
final StringTokenizer lineTok = new StringTokenizer(randomizeStr, ".", false);
while (lineTok.hasMoreTokens())
{
final String aString = lineTok.nextToken();
if (aString.startsWith("AGECAT"))
{
generateAge(Integer.parseInt(aString.substring(6)), false, pc);
}
else
{
ranList.add(aString);
}
}
if (ranList.contains("AGE"))
{
generateAge(0, true, pc);
}
if (ranList.contains("HT") || ranList.contains("WT"))
{
generateHeightWeight(pc);
}
if (ranList.contains("EYES"))
{
pc.setEyeColor(generateBioValue("EYES", pc));
}
if (ranList.contains("HAIR"))
{
pc.setPCAttribute(PCAttribute.HAIRCOLOR, generateBioValue("HAIR", pc));
}
if (ranList.contains("SKIN"))
{
pc.setPCAttribute(PCAttribute.SKINCOLOR, generateBioValue("SKINTONE", pc));
}
}
@Override
public String toString()
{
final StringBuilder sb = new StringBuilder(100);
sb.append("AgeMap: ").append(ageMap.toString()).append("\n");
sb.append("UserMap: ").append(userMap.toString()).append("\n");
return sb.toString();
}
private static String replaceString(final String argInput, final String replacement, final int value)
{
String input = argInput;
final int x = input.indexOf(replacement);
if (x >= 0)
{
final String output = input.substring(0, x);
final String appendage = input.substring(x + replacement.length());
input = output + value + appendage;
}
return input;
}
/**
* Retrieves a collection of the tags defined for a race grouped by
* the age brackets.
*
* @param region The region of the race
* @param race The name of the race.
* @return SortedMap A map of the gae brackets. Within each age bracket is a
* sorted map of the races (one only) and wihtin this is the tags for that
* race and age.
*/
private SortedMap<Integer, SortedMap<String, SortedMap<String, String>>> getRaceTagsByAge(Region region, String race)
{
// setup a mapped structure
final SortedMap<Integer, SortedMap<String, SortedMap<String, String>>> ageSets = new TreeMap<>();
// Read in the user settings, split where necessary and add to the appropriate age bracket
for (String key : userMap.getTertiaryKeySet(region, race))
{
addTagToAgeSet(ageSets, race, key, userMap.getListFor(region, race, key));
}
return ageSets;
}
private String getTokenNumberInMaps(final String addKey, final int tokenNum, String regionName, String raceName)
{
final List<String> r = getValueInMaps(regionName, raceName, addKey);
if (r == null)
{
return null;
}
if (r.size() <= tokenNum)
{
return "0";
}
return r.get(tokenNum);
}
public List<String> getValueInMaps(final String argRegionName, final String argRaceName, final String addKey)
{
final String anotherRaceName;
if (argRaceName.indexOf('(') >= 0)
{
anotherRaceName = argRaceName.substring(0, argRaceName.indexOf('(')).trim() + '%';
}
else
{
anotherRaceName = argRaceName + '%';
}
return mapFind(userMap, argRegionName, argRaceName, addKey, anotherRaceName);
}
/**
* Adds the tag (key & value) to the supplied ageSets collection. It is
* assumed that the ageSet already has an entry for each age bracket and
* that this entry will be a SortedMap of races. Each race will contain a
* SortedMap of tags and their values.<br> The key is assumed to be of the
* form region.race.tag eg "Custom.Human%.MAXAGE" The value is assumed to be
* either a list of values or a single value, depending on the tag. eg
* "[34,52,69,110]" or "Blond|Brown" If a single value, it will be added to
* the first age set. Multiple values are split amongst the age sets in
* order, with any values not matching an age set being ignored.
*
* @param ageSets
* The collection of age brackets.
* @param key
* The region.race.tag specifier.
* @param value
* The value of the tag.
*/
private void addTagToAgeSet(
final SortedMap<Integer, SortedMap<String, SortedMap<String, String>>> ageSets,
String race, String key, final List<String> value)
{
final Iterator<String> iter = value.iterator();
for (int ageBracket : ageNames.values())
{
if (!iter.hasNext())
{
break;
}
final String tagValue = iter.next();
SortedMap<String, SortedMap<String, String>> races = ageSets
.get(ageBracket);
if (races == null)
{
races = new TreeMap<>();
ageSets.put(ageBracket, races);
}
SortedMap<String, String> tags = races.get(race);
if (tags == null)
{
tags = new TreeMap<>();
races.put(race, tags);
}
tags.put(key, tagValue);
}
}
private String appendAgesetInfo(Region region,
final SortedMap<Integer, SortedMap<String, SortedMap<String, String>>> ageSets,
final StringBuilder sb)
{
Set<Integer> ageIndices = new TreeSet<>();
ageIndices.addAll(ageSets.keySet());
ageIndices.addAll(ageNames.values());
// Iterate through ages, outputing the info
for (Integer key : ageIndices)
{
final SortedMap<String, SortedMap<String, String>> races = ageSets.get(key);
if (races == null)
{
continue;
}
sb.append("AGESET:");
sb.append(ageMap.get(region, key).getLSTformat()).append("\n");
for (final Map.Entry<String, SortedMap<String, String>> stringSortedMapEntry : races.entrySet())
{
if (!"AGESET".equals(stringSortedMapEntry.getKey()))
{
final SortedMap<String, String> tags = stringSortedMapEntry.getValue();
for (final Map.Entry<String, String> stringStringEntry : tags.entrySet())
{
sb.append("RACENAME:").append(stringSortedMapEntry.getKey()).append("\t\t");
sb.append(stringStringEntry.getKey()).append(':').append(stringStringEntry.getValue()).append("\n");
}
}
}
sb.append("\n");
}
return sb.toString();
}
private void generateAge(final int ageCategory, final boolean useClassOnly, final PlayerCharacter pc)
{
// Can't find a base age for the category,
// then there's nothing to do
final String age = getTokenNumberInMaps("BASEAGE", ageCategory, pc
.getDisplay().getRegionString(), pc.getRace().getKeyName().trim());
if (age == null)
{
return;
}
// First check for class age modification information
final int baseAge = Integer.parseInt(age);
int ageAdd = -1;
String aClass = getTokenNumberInMaps("CLASS", ageCategory, pc
.getDisplay().getRegionString(), pc.getRace().getKeyName().trim());
if (aClass != null && !aClass.equals("0"))
{
// aClass looks like:
// Barbarian,Rogue,Sorcerer[BASEAGEADD:3d6]|Bard,Fighter,Paladin,Ranger[BASEAGEADD:1d6]
// So first, get the BASEAGEADD
final StringTokenizer aTok = new StringTokenizer(aClass, "|");
while (aTok.hasMoreTokens())
{
// String looks like:
// Barbarian,Rogue,Sorcerer[BASEAGEADD:3d6]
String aString = aTok.nextToken();
final int start = aString.indexOf("[");
final int end = aString.indexOf("]");
// should be BASEAGEADD:xdy
String dieString = aString.substring(start + 1, end);
if (dieString.startsWith("BASEAGEADD:"))
{
dieString = dieString.substring(11);
}
// Remove the dieString
aString = aString.substring(0, start);
final StringTokenizer bTok = new StringTokenizer(aString, ",");
while (bTok.hasMoreTokens() && (ageAdd < 0))
{
final String tClass = bTok.nextToken();
if (pc.getClassKeyed(tClass) != null)
{
ageAdd = RollingMethods.roll(dieString);
}
}
}
}
// If there was no class age modification,
// then generate a number based on the .LST
if ((ageAdd < 0) && !useClassOnly)
{
aClass = getTokenNumberInMaps("AGEDIEROLL", ageCategory, pc
.getDisplay().getRegionString(), pc.getRace().getKeyName().trim());
if (aClass != null)
{
ageAdd = RollingMethods.roll(aClass);
}
}
if ((ageAdd >= 0) && (baseAge > 0))
{
final String maxage = getTokenNumberInMaps("MAXAGE", ageCategory, pc
.getDisplay().getRegionString(), pc.getRace().getKeyName().trim());
if (maxage != null)
{
final int maxAge = Integer.parseInt(maxage);
if (baseAge + ageAdd > maxAge)
{
ageAdd = maxAge-baseAge;
}
}
pc.setPCAttribute(NumericPCAttribute.AGE, baseAge + ageAdd);
}
}
private String generateBioValue(final String addKey, final PlayerCharacter pc)
{
final String line = getTokenNumberInMaps(addKey, 0, pc.getDisplay().getRegionString(), pc
.getRace().getKeyName().trim());
final String rv;
if (line != null && !line.isEmpty())
{
final StringTokenizer aTok = new StringTokenizer(line, "|");
final List<String> aList = new ArrayList<>();
while (aTok.hasMoreTokens())
{
aList.add(aTok.nextToken());
}
final int roll = RollingMethods.roll(1, aList.size()) - 1; // needs to be 0-offset
rv = aList.get(roll);
}
else
{
rv = "";
}
return rv;
}
private void generateHeightWeight(final PlayerCharacter pc)
{
int baseHeight = 0;
int baseWeight = 0;
int htAdd = 0;
int wtAdd = 0;
String totalWeight = null;
final String htwt = getTokenNumberInMaps("SEX", 0, pc.getDisplay().getRegionString(), pc
.getRace().getKeyName().trim());
if (htwt == null)
{
return;
}
final StringTokenizer genderTok = new StringTokenizer(htwt, "[]", false);
while (genderTok.hasMoreTokens())
{
if (genderTok.nextToken().equals(pc.getDisplay().getGenderObject().toString()))
{
final String htWtLine = genderTok.nextToken();
final StringTokenizer htwtTok = new StringTokenizer(htWtLine, "|", false);
while (htwtTok.hasMoreTokens())
{
final String tag = htwtTok.nextToken();
if (tag.startsWith("BASEHT:"))
{
baseHeight = Integer.parseInt(tag.substring(7));
}
else if (tag.startsWith("BASEWT:"))
{
baseWeight = Integer.parseInt(tag.substring(7));
}
else if (tag.startsWith("HTDIEROLL:"))
{
htAdd = RollingMethods.roll(tag.substring(10));
}
else if (tag.startsWith("WTDIEROLL:"))
{
wtAdd = RollingMethods.roll(tag.substring(10));
}
else if (tag.startsWith("TOTALWT:"))
{
totalWeight = tag.substring(8);
}
}
if ((baseHeight != 0) && (htAdd != 0))
{
pc.setHeight(baseHeight + htAdd);
}
if ((totalWeight != null) && (baseWeight != 0) && (wtAdd != 0))
{
totalWeight = replaceString(totalWeight, "HTDIEROLL", htAdd);
totalWeight = replaceString(totalWeight, "BASEWT", baseWeight);
totalWeight = replaceString(totalWeight, "WTDIEROLL", wtAdd);
pc.setPCAttribute(NumericPCAttribute.WEIGHT, pc.getVariableValue(totalWeight, "").intValue());
}
break;
}
genderTok.nextToken(); // burn next token
}
}
private List<String> mapFind(
final TripleKeyMapToList<Region, String, String, String> argMap,
final String argRegionName, final String argRaceName,
final String addKey, final String altRaceName)
{
// First check for region.racename.key
Region region = Region.getConstant(argRegionName);
List<String> r = argMap.getListFor(region, argRaceName, addKey);
if (r != null && !r.isEmpty())
{
return r;
}
//
// If not found, try the race name without any parenthesis
//
final int altRaceLength = altRaceName.length();
if (altRaceLength != 0)
{
r = argMap.getListFor(region, altRaceName, addKey);
if (r != null)
{
return r;
}
}
//
// If still not found, try the same two searches again without a region
//
if (!argRegionName.equals(Constants.NONE))
{
region = Region.getConstant("None");
r = argMap.getListFor(region, argRaceName, addKey);
if (r != null)
{
return r;
}
if (altRaceLength != 0)
{
r = argMap.getListFor(region, altRaceName, addKey);
}
}
return r;
}
public AgeSet addToAgeMap(String regionName, AgeSet ageSet, URI sourceURI)
{
AgeSet old =
ageMap.get(Region.getConstant(regionName), ageSet.getIndex());
if (old != null)
{
if (ageSet.hasBonuses() || !ageSet.getKits().isEmpty() || !ageSet.getName().equals(old.getName()))
{
Logging.errorPrint("Found second (non-identical) AGESET "
+ "in Bio Settings " + sourceURI + " for Region: "
+ regionName + " Index: " + ageSet.getIndex()
+ " using the existing " + old.getLSTformat());
}
return old;
}
ageMap.put(Region.getConstant(regionName), ageSet.getIndex(), ageSet);
return ageSet;
}
public Integer addToNameMap(AgeSet ageSet)
{
return ageNames.put(ageSet.getName(), ageSet.getIndex());
}
public Set<String> getAgeCategories()
{
Set<String> set = new TreeSet<>();
for (Object o : ageNames.keySet())
{
set.add(o.toString());
}
return set;
}
public Map<Integer, AgeSet> getAgeSets(String regionName)
{
return new TreeMap<>(ageMap.getMapFor(Region.getConstant(regionName)));
}
}