/*
* Copyright 2001 (C) Bryan McRoberts <merton_monk@yahoo.com>
* Copyright 2005 (C) Tom Parker <thpr@sourceforge.net>
*
* 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
*
* Refactored out of PObject July 22, 2005
*
* Current Ver: $Revision$
*/
package pcgen.core.prereq;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import pcgen.base.util.WrappedMapSet;
import pcgen.cdom.base.CDOMObject;
import pcgen.cdom.base.CDOMReference;
import pcgen.cdom.base.ChooseInformation;
import pcgen.cdom.base.Constants;
import pcgen.cdom.content.CNAbility;
import pcgen.cdom.enumeration.FormulaKey;
import pcgen.cdom.enumeration.ListKey;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.core.Ability;
import pcgen.core.AbilityCategory;
import pcgen.core.Domain;
import pcgen.core.Equipment;
import pcgen.core.Globals;
import pcgen.core.PlayerCharacter;
import pcgen.core.SettingsHandler;
import pcgen.core.Skill;
import pcgen.core.WeaponProf;
import pcgen.core.spell.Spell;
import pcgen.util.Logging;
/**
* @author Tom Parker <thpr@sourceforge.net>
*
* This is a utility class related to PreReq objects.
*/
public final class PrerequisiteUtilities
{
/**
* Private Constructor, prevents instantiation.
*/
private PrerequisiteUtilities()
{
// Don't allow instantiation of utility class
}
/**
* Tests a list of prerequisites against a given PC and a given Source. It then
* generates an HTML representation of whether they passed.
* @param aPC The PC to test the prerequisites against.
* @param aObj The source of the PreRequisite.
* @param aList A list of prerequisite objects.
* @param includeHeader Whether to wrap the generated string in html tags.
* @return An HTML representation of whether a set of PreRequisites passed for a given PC and Source.
*/
public static String preReqHTMLStringsForList(
final PlayerCharacter aPC,
final CDOMObject aObj,
final Collection<Prerequisite> aList,
final boolean includeHeader)
{
if ((aList == null) || aList.isEmpty())
{
return "";
}
final StringBuilder pString = new StringBuilder(aList.size() * 20);
final List<Prerequisite> newList = new ArrayList<>();
boolean first = true;
for (Prerequisite prereq : aList)
{
newList.clear();
newList.add(prereq);
if (first)
{
first = false;
}
else
{
pString.append(" and ");
}
final String bString = PrereqHandler.toHtmlString(newList);
final boolean passes;
if (aObj instanceof Equipment)
{
passes = PrereqHandler.passesAll(newList, (Equipment) aObj, aPC);
}
else
{
passes = PrereqHandler.passesAll(newList, aPC, null);
}
if (!passes)
{
pString.append(SettingsHandler.getPrereqFailColorAsHtmlStart());
pString.append("<i>");
}
final StringTokenizer aTok = new StringTokenizer(bString, "&<>", true);
while (aTok.hasMoreTokens())
{
final String aString = aTok.nextToken();
if (aString.equals("<"))
{
pString.append("<");
}
else if (aString.equals(">"))
{
pString.append(">");
}
else if (aString.equals("&"))
{
pString.append("&");
}
else
{
pString.append(aString);
}
}
if (!passes)
{
pString.append("</i>");
pString.append(SettingsHandler.getPrereqFailColorAsHtmlEnd());
}
}
if (pString.toString().indexOf('<') >= 0)
{
// seems that ALIGN and STAT have problems in
// HTML display, so wrapping in <font> tag.
pString.insert(0, "<font>");
pString.append("</font>");
if (includeHeader)
{
if (pString.toString().indexOf('<') >= 0)
{
pString.insert(0, "<html>");
pString.append("</html>");
}
}
}
String result = pString.toString().replaceAll("##BR##", "<br>");
return result;
}
/**
* Check if the character passes the ability prerequisite. Refactored here
* for use by both PREFEAT and PREABILITY.
*
* @param prereq The prerequisite to be run.
* @param character The character to be checked.
* @param numMatches The number of matches required.
* @param categoryName The name of the required category, null if any category will be matched.
* @return The number of matches made, 0 if not enough matches were made.
*/
public static int passesAbilityTest(
final Prerequisite prereq,
final PlayerCharacter character,
final int numMatches,
String categoryName)
{
final boolean countMults = prereq.isCountMultiples();
final boolean keyIsAny = prereq.getKey().equalsIgnoreCase(Constants.LST_ANY);
final boolean keyIsType = isTypeTest(prereq.getKey());
final String strippedKey = keyIsType
? prereq.getKey().substring(Constants.SUBSTRING_LENGTH_FIVE)
: prereq.getKey();
int runningTotal = 0;
final Set<Ability> abilityList = buildAbilityList(character, categoryName);
if (!abilityList.isEmpty())
{
for (Ability ability : abilityList)
{
final String abilityKey = ability.getKeyName();
if (keyIsAny
|| (!keyIsType && abilityKey.equalsIgnoreCase(strippedKey))
|| (keyIsType && ability.isType(strippedKey)))
{
// either this feat has matched on the name, or the type
if (prereq.getSubKey() != null)
{
runningTotal += dealWithSubKey(
prereq,
character,
strippedKey,
ability);
}
else
{
// subKey == null
runningTotal++;
if (ability.getSafe(ObjectKey.MULTIPLE_ALLOWED) && countMults)
{
/*
* SERVESAS occurrences might mean this is less than zero,
* in which case ignore it. This still leaves the instance
* where more than one of an item is desired, and one
* instance is a SERVESAS, but that is a high cost corner case.
*/
List<String> assocs =
character
.getConsolidatedAssociationList(ability);
int select =
ability.getSafe(FormulaKey.SELECT)
.resolve(character, "").intValue();
int num = (assocs.size() / select) - 1;
if (num > 0)
{
runningTotal += num;
}
}
}
}
else
{
if (prereq.getSubKey() != null)
{
final int len = Constants.SUBSTRING_LENGTH_FIVE;
final boolean subKeyIsType = isTypeTest(prereq.getSubKey());
final String subKey = subKeyIsType
? prereq.getSubKey().substring(len)
: prereq.getSubKey();
final String s1 = strippedKey + " (" + subKey + ")";
final String s2 = strippedKey + "(" + subKey + ")";
if (abilityKey.equalsIgnoreCase(s1)
|| ability.getKeyName().equalsIgnoreCase(s2))
{
runningTotal++;
if (!countMults)
{
break;
}
}
}
}
}
}
runningTotal = prereq.getOperator().compare(runningTotal, numMatches);
return runningTotal;
}
/**
* This operation deals with matching subKeys against abilities where the main key
* has already matched.
* The subKey may be prefixed with TYPE(=|.) in which case the Choice string from the
* ability will be used to check for the type of chooser. Possibilities are SKILL,
* WEAPONPROFICIENCY, DOMAIN, or SPELL. A list of keys will be retrieved from the ability's
* associated object list. The objects matching these keys are retrieved and checked
* for type against subKey. A count is returned (respects countMults).
*
* If the subKey is not specifying a type, then the key of the prerequisite is checked
* against the key of the ability, if they match and the ability object is associated
* with the character via the subkey, then a count of the number of instances is returned.
*
* Finally,the subkey may specify a wildcard. If it does, the list of associations
* between the ability object and the PC are checked. A count is returned of teh number
* that begin with the wildcard string.
*
* @param prereq The prerequisite to be checked.
* @param character The character to be checked.
* @param key the Key from the prerequisite which has been stripped the prefixes TYPE= abd TYPE.
* @param ability The ability being checked for a match.
* @return A count which respects countMults.
*/
private static int dealWithSubKey(
Prerequisite prereq,
PlayerCharacter character,
String key,
Ability ability)
{
final boolean countMults = prereq.isCountMultiples();
int runningTotal = 0;
final String subKey = prereq.getSubKey();
final boolean subKeyIsType = isTypeTest(subKey);
final int wildCardPos = subKey.indexOf('%');
List<String> assocs = character.getConsolidatedAssociationList(ability);
if (subKeyIsType)
{
final String type = prereq.getSubKey().substring(Constants.SUBSTRING_LENGTH_FIVE);
runningTotal = countSubKeyType(character, ability, type, countMults);
}
else if (ability.getKeyName().equalsIgnoreCase(key)
&& hasAssoc(assocs, subKey))
{
if (countMults && ability.getSafe(ObjectKey.MULTIPLE_ALLOWED))
{
int select = ability.getSafe(FormulaKey.SELECT).resolve(character, "").intValue();
int countMatchingSubKey = countSubkeyMatches(assocs, subKey);
runningTotal = countMatchingSubKey / select;
}
else
{
runningTotal = 1;
}
}
else if (wildCardPos > -1)
{
String preWildcard = (wildCardPos == 0)
? Constants.EMPTY_STRING
: subKey.substring(0, wildCardPos).toUpperCase();
runningTotal = countSubKeyWildcardMatch(
character,
countMults,
preWildcard,
ability);
}
return runningTotal;
}
private static int countSubkeyMatches(List<String> assocs, String subKey)
{
int numMatches = 0;
for (String s : assocs)
{
if (subKey.equalsIgnoreCase(s))
{
numMatches++;
}
}
return numMatches;
}
private static boolean hasAssoc(List<String> assocs, String subKey)
{
for (String s : assocs)
{
if (subKey.equalsIgnoreCase(s))
{
return true;
}
}
return false;
}
/**
* Is this string a Type selector?
* @param key the string to test
* @return true if key begins with TYPE= or TYPE.
*/
private static boolean isTypeTest(String key)
{
return key.startsWith(Constants.LST_TYPE_EQUAL) || key.startsWith(Constants.LST_TYPE_DOT);
}
/**
* Having matched the ability on the other criteria, check for a match
* against the subKey.
*
* @param character
* The character being tested.
* @param countMults
* Should multiple occurrences be counted?
* @param preWilcard
* The portion of the prerequisite's subkey that appears
* before the wilcard character '%'
* @param ability
* The ability being checked for a match.
* @return The number of matches made
*/
private static int countSubKeyWildcardMatch(
final PlayerCharacter character,
final boolean countMults,
String preWilcard,
Ability ability)
{
int runningTotal = 0;
for (String assoc : character.getConsolidatedAssociationList(ability))
{
final String fString = assoc.toUpperCase();
if (preWilcard.isEmpty() || fString.startsWith(preWilcard))
{
runningTotal++;
if (!countMults)
{
break;
}
}
}
return runningTotal;
}
private static int countSubKeyType(
PlayerCharacter aPC,
Ability ability, String type, boolean countMults)
{
final List<String> selectedList = aPC.getConsolidatedAssociationList(ability);
ChooseInformation<?> chooseInformation = ability.getSafe(ObjectKey.CHOOSE_INFO);
final String aChoiceString = chooseInformation.getName();
if (aChoiceString.startsWith("SKILL")) //$NON-NLS-1$
{
return subKeySkill(countMults, type, selectedList);
}
else if (aChoiceString.startsWith("WEAPONPROFICIENCY")) //$NON-NLS-1$
{
return subKeyWeaponProf(countMults, type, selectedList);
}
else if (aChoiceString.startsWith("DOMAIN")) //$NON-NLS-1$
{
return subKeyDomain(countMults, type, selectedList);
}
else if (aChoiceString.startsWith("SPELL")) //$NON-NLS-1$
{
return subKeySpell(countMults, type, selectedList);
}
return 0;
}
/**
* Build up a list of the character's abilities which match the category requirements.
*
* @param character The character to be tested.
* @param categoryName The name of the required category, null if any category will be matched.
* @return A list of categories matching.
*/
private static Set<Ability> buildAbilityList(
final PlayerCharacter character,
String categoryName)
{
final Set<Ability> abilityList = new WrappedMapSet<>(IdentityHashMap.class);
if (character != null)
{
AbilityCategory cat = SettingsHandler.getGame().getAbilityCategory(categoryName);
if (cat == null)
{
Logging.errorPrint("Invalid category " + categoryName + " in PREABILITY");
return abilityList;
}
if (!cat.getParentCategory().equals(cat))
{
Logging.errorPrint("Invalid use of child category in PREABILITY");
}
for (CNAbility cna : character.getCNAbilities(cat))
{
abilityList.add(cna.getAbility());
}
Collection<AbilityCategory> allCats =
SettingsHandler.getGame().getAllAbilityCategories();
// Now scan for relevant SERVESAS occurrences
for (AbilityCategory aCat : allCats)
{
for (CNAbility cna : character.getPoolAbilities(aCat))
{
for(CDOMReference<Ability> ref
: cna.getAbility().getSafeListFor(ListKey.SERVES_AS_ABILITY))
{
for (Ability ab : ref.getContainedObjects())
{
abilityList.add(ab);
}
}
}
}
}
return abilityList;
}
/**
* Count the number of spells associated with the ability being tested of types cType.
*
* @param countMults Should multiple occurrences be counted?
* @param cType The type to check for.
* @param selectedList The list of spells associated with the ability being tested.
* @return int
*/
private static int subKeySpell(
final boolean countMults,
final String cType,
final List<String> selectedList)
{
int returnTotal = 0;
for (String spell : selectedList)
{
//TODO Case sensitivity?
final Spell sp = Globals.getContext().getReferenceContext()
.silentlyGetConstructedCDOMObject(Spell.class, spell);
if (sp == null)
{
continue;
}
if (sp.isType(cType))
{
returnTotal++;
if (!countMults)
{
break;
}
}
}
return returnTotal;
}
/**
* Count the number of domains associated with the ability being tested of types cType.
*
* @param countMults Should multiple occurrences be counted?
* @param cType The type to check for.
* @param selectedList The list of domains associated with the ability being tested.
* @return int
*/
private static int subKeyDomain(
final boolean countMults,
final String cType,
final List<String> selectedList)
{
int returnTotal = 0;
for (String domain : selectedList)
{
final Domain dom;
dom = Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(Domain.class, domain);
if (dom == null)
{
continue;
}
if (dom.isType(cType))
{
returnTotal++;
if (!countMults)
{
break;
}
}
}
return returnTotal;
}
/**
* Count the number of weaponprofs associated with the ability being tested of types cType.
*
* @param countMults Should multiple occurrences be counted?
* @param cType The type to check for.
* @param selectedList The list of weaponprofs associated with the ability being tested.
* @return int
*/
private static int subKeyWeaponProf(
final boolean countMults,
final String cType,
final List<String> selectedList)
{
int returnTotal = 0;
for (String weaponprof : selectedList)
{
final WeaponProf wp = Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(
WeaponProf.class, weaponprof);
if (wp == null)
{
continue;
}
if (wp.isType(cType))
{
returnTotal++;
if (!countMults)
{
break;
}
continue;
}
final Equipment eq = Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(
Equipment.class, wp.getKeyName());
if (eq == null)
{
continue;
}
if (eq.isType(cType))
{
returnTotal++;
if (!countMults)
{
break;
}
}
}
return returnTotal;
}
/**
* Count the number of skills associated with the ability being tested of types cType.
*
* @param countMults Should multiple occurrences be counted?
* @param cType The type to check for.
* @param selectedList The list of skills associated with the ability being tested.
* @return int
*/
private static int subKeySkill(
final boolean countMults,
final String cType,
final List<String> selectedList)
{
int returnTotal = 0;
for (String skill : selectedList)
{
final Skill sk;
sk = Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(Skill.class, skill);
if (sk == null)
{
continue;
}
if (sk.isType(cType))
{
returnTotal++;
if (!countMults)
{
break;
}
}
}
return returnTotal;
}
/**
* Identify if the prerequisite is itself of the supplied kind or has a
* descendant of the required kind.
* @param prereq Prerequisite to be checked
* @param matchKind Kind to be checked for.
* @return true if we got as match.
*/
public static boolean hasPreReqKindOf(final Prerequisite prereq, String matchKind)
{
if (prereq == null)
{
return false;
}
if (matchKind == prereq.getKind()
|| matchKind.equalsIgnoreCase(prereq.getKind()))
{
return true;
}
for (Prerequisite childPrereq : prereq.getPrerequisites())
{
if (hasPreReqKindOf(childPrereq, matchKind))
{
return true;
}
}
return false;
}
/**
* Identify if the prerequisite is itself of the supplied kind or has a
* descendant of the required kind.
* @param prereq Prerequisite to be checked
* @param matchKind Kind to be checked for.
* @return true if we got as match.
*/
public static Collection<Prerequisite> getPreReqsOfKind(final Prerequisite prereq, String matchKind)
{
Set<Prerequisite> matchingPrereqs = new HashSet<>();
if (prereq == null)
{
return matchingPrereqs;
}
if (matchKind == prereq.getKind()
|| matchKind.equalsIgnoreCase(prereq.getKind()))
{
matchingPrereqs.add(prereq);
}
for (Prerequisite childPrereq : prereq.getPrerequisites())
{
matchingPrereqs.addAll(getPreReqsOfKind(childPrereq, matchKind));
}
return matchingPrereqs;
}
/**
* Identify if the prerequisite is itself of the supplied kind or has a
* descendant of the required kind. Kind is either FEAT or ABILITY, the
* key is the name of an ability object.
*
* @param prereq Prerequisite to be checked
* @param matchKind Kind to be checked for.
* @param matchKey The name of an ability object.
* @return true if we got a match
*/
public static boolean hasPreReqMatching(
final Prerequisite prereq,
String matchKind,
String matchKey)
{
if (prereq == null)
{
return false;
}
if ((matchKind == prereq.getKind())
|| (matchKind.equalsIgnoreCase(prereq.getKind())))
{
if ((matchKey == prereq.getKey())
|| (matchKey.equalsIgnoreCase(prereq.getKey())))
{
return true;
}
}
for (Prerequisite childPrereq : prereq.getPrerequisites())
{
if (hasPreReqMatching(childPrereq, matchKind, matchKey))
{
return true;
}
}
return false;
}
}