/*
* BonusObj.java
* Copyright 2002 (C) Greg Bingleman <byngl@hotmail.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 December 13, 2002, 9:19 AM
*
* Current Ver: $Revision$
*
*/
package pcgen.core.bonus;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import pcgen.base.formula.Formula;
import pcgen.cdom.base.ConcretePrereqObject;
import pcgen.cdom.base.Constants;
import pcgen.cdom.base.FormulaFactory;
import pcgen.cdom.base.QualifyingObject;
import pcgen.core.PlayerCharacter;
import pcgen.core.utils.CoreUtility;
import pcgen.persistence.lst.output.prereq.PrerequisiteWriter;
import pcgen.rules.context.LoadContext;
/**
* {@code BonusObj}
*
* @author Greg Bingleman <byngl@hotmail.com>
**/
public abstract class BonusObj extends ConcretePrereqObject implements Serializable, Cloneable, QualifyingObject
{
private List<Object> bonusInfo = new ArrayList<>();
private Map<String, String> dependMap = new HashMap<>();
private Formula bonusFormula = FormulaFactory.ZERO;
/** The name of the bonus e.g. STAT or COMBAT */
private String bonusName = Constants.EMPTY_STRING;
/** The type of the bonus e.g. Enhancement or Dodge */
private String bonusType = Constants.EMPTY_STRING;
private String varPart = Constants.EMPTY_STRING;
private String typeOfBonus = Bonus.BONUS_UNDEFINED;
private String stringRepresentation = null;
private String tokenSource = null;
/** An enum for the possible stacking modifiers a bonus can have */
public enum StackType {
/** This bonus will follow the normal stacking rules. */
NORMAL,
/** This bonus will always stack regardless of its type. */
STACK,
/**
* This bonus will stack with other bonuses of its own type but not
* with bonuses of other types.
*/
REPLACE
}
private StackType theStackingFlag = StackType.NORMAL;
/**
* Get Bonus Info
* @return Bonus info
*/
public String getBonusInfo()
{
final StringBuilder sb = new StringBuilder(50);
if (!bonusInfo.isEmpty())
{
for (int i = 0; i < bonusInfo.size(); ++i)
{
sb.append(i == 0 ? Constants.EMPTY_STRING : Constants.COMMA);
sb.append(unparseToken(bonusInfo.get(i)));
}
}
else
{
sb.append("|ERROR"); //$NON-NLS-1$
}
return sb.toString().toUpperCase();
}
/**
* Return a list of the unparsed (converted back to strings)
* bonus info entries.
* @return The unparsed bonus info list
*/
public List<String> getUnparsedBonusInfoList()
{
List<String> list = new ArrayList<>();
for (Object info : bonusInfo)
{
list.add(unparseToken(info));
}
return list;
}
/**
* get Bonus Info List
* @return Bonus Info List
*/
public List<?> getBonusInfoList()
{
return bonusInfo;
}
/**
* Get Bonus Name
* @return bonus name
*/
public String getBonusName()
{
return bonusName;
}
/**
* Get depends on given a key
* @param aString
* @return true if it depends on
*/
public boolean getDependsOn(final String aString)
{
return dependMap.containsKey(aString);
}
/**
* Get depends on given a set of keys
* @param aList List of bonus keys
* @return true if it any of the keys are depended on
*/
public boolean getDependsOn(final List<String> aList)
{
for (String key : aList)
{
if (dependMap.containsKey(key))
{
return true;
}
}
return false;
}
/**
* Check if this bonus requires bonuses of a particular name to be
* resolved first. e.g. Supply STAT to check for all BONUS:STAT entries.
* @param bonusName Bonus name to be checked for.
* @return true if there is a dependancy
*/
public boolean getDependsOnBonusName(final String bonusName)
{
return dependMap.containsKey("NAME|"+bonusName);
}
/**
* Report on the dependencies of the bonus.
* @return String the dependancies
*/
public String listDependsMap()
{
StringBuilder buff = new StringBuilder("[");
for (String key : dependMap.keySet())
{
if (buff.length()> 1)
{
buff.append(", ");
}
buff.append(key);
}
buff.append("]");
return buff.toString();
}
/**
* @return type of Bonus
*/
public String getTypeOfBonus()
{
return typeOfBonus;
}
/**
* Get the bonus type
* @return the bonus type
*/
public String getTypeString()
{
return bonusType;
}
/**
* Set value
* @param bValue
*/
Formula setValue(final String bValue)
{
bonusFormula = FormulaFactory.getFormulaFor(bValue);
if (!bonusFormula.isStatic())
{
buildDependMap(bValue.toUpperCase());
}
return bonusFormula;
}
/**
* Get the bonus formula
* @return the formula
*/
public Formula getFormula()
{
return bonusFormula;
}
/**
* Get the bonus value
* @return the value
*/
public String getValue()
{
return bonusFormula.toString();
}
/**
* Get the bonus value as a double
* @param string
* @return bonus value as a double
*/
public Number resolve(PlayerCharacter pc, String string)
{
return bonusFormula.resolve(pc, string);
}
/**
* is value static
* @return true if value is static
*/
public boolean isValueStatic()
{
return bonusFormula.isStatic();
}
/**
* Set the variable
* @param aString
*/
public void setVariable(final String aString)
{
varPart = aString.toUpperCase();
}
/**
* Get the variable
* @return the variable
*/
public String getVariable()
{
return varPart;
}
/**
* Has bonus type string
* @return true if bonus type string exists
*/
public boolean hasTypeString()
{
return !bonusType.isEmpty();
}
/**
* has variable
* @return true if bonus has variable
*/
public boolean hasVariable()
{
return !varPart.isEmpty();
}
/**
* Retrieve the persistent text for this bonus. This text
* when reparsed will recreate the bonus. PCC Text is used
* as a name here as this output could also be used by the
* LST editors.
* @return The text to be saved for example in a character.
*/
public String getPCCText()
{
if (stringRepresentation == null)
{
final StringBuilder sb = new StringBuilder(50);
sb.append(getTypeOfBonus());
if (varPart != null && !varPart.isEmpty())
{
sb.append(varPart);
}
if (!bonusInfo.isEmpty())
{
for (int i = 0; i < bonusInfo.size(); ++i)
{
sb.append(i == 0 ? '|' : ',').append(
unparseToken(bonusInfo.get(i)));
}
}
else
{
sb.append("|ERROR");
}
sb.append('|').append(bonusFormula.toString());
if (!bonusType.isEmpty())
{
sb.append("|TYPE=").append(bonusType);
}
// And put the prereqs at the end of the string.
if (hasPrerequisites())
{
sb.append(Constants.PIPE);
sb.append(new PrerequisiteWriter().getPrerequisiteString(
getPrerequisiteList(), Constants.PIPE));
}
stringRepresentation = sb.toString();
}
return stringRepresentation;
}
/**
* @see Object#toString()
*/
@Override
public String toString()
{
return getPCCText();
}
protected void setBonusName(final String aName)
{
bonusName = aName;
}
protected void setTypeOfBonus(final String type)
{
typeOfBonus = type;
}
protected void addBonusInfo(final Object obj)
{
bonusInfo.add(obj);
}
protected void replaceBonusInfo(final Object oldObj, final Object newObj)
{
for (int i = 0; i < bonusInfo.size(); ++i)
{
final Object curObj = bonusInfo.get(i);
if (curObj == oldObj)
{
bonusInfo.set(i, newObj);
break;
}
}
}
protected boolean addType(final String typeString)
{
if (bonusType.isEmpty())
{
bonusType = typeString.toUpperCase();
return true;
}
return false;
}
/**
* Sets the stacking flag for this bonus.
*
* @param aFlag A <tt>StackType</tt> to set.
*/
public void setStackingFlag( final StackType aFlag )
{
theStackingFlag = aFlag;
}
/**
* Gets the stacking flag for this bonus.
*
* @return A <tt>StackType</tt>.
*/
public StackType getStackingFlag()
{
return theStackingFlag;
}
protected abstract boolean parseToken(LoadContext context, final String token);
protected abstract String unparseToken(final Object obj);
public abstract String getBonusHandled();
private void buildDependMap(String aString)
{
addImpliedDependenciesFor(aString);
// First whack out all the () pairs to find variable names
while (aString.lastIndexOf('(') >= 0)
{
final int x = CoreUtility.innerMostStringStart(aString);
final int y = CoreUtility.innerMostStringEnd(aString);
if (y < x)
{
return;
}
final String bString = aString.substring(x + 1, y);
buildDependMap(bString);
aString =
new StringBuilder(aString.length())
.append(aString.substring(0, x))
.append(aString.substring(y + 1)).toString();
}
if (aString.indexOf("(") >= 0 || aString.indexOf(")") >= 0 ||
aString.indexOf("%") >= 0)
{
return;
}
// We now have the substring we want to work on
final StringTokenizer cTok = new StringTokenizer(aString, ".,");
while (cTok.hasMoreTokens())
{
final String controlString = cTok.nextToken();
// skip flow control tags
if ("IF".equals(controlString) || "THEN".equals(controlString) || "ELSE"
.equals(controlString)
|| "GT".equals(controlString) || "GTEQ".equals(controlString) || "EQ"
.equals(controlString)
|| "LTEQ".equals(controlString) || "LT".equals(controlString))
{
continue;
}
// Now remove math strings: + - / *
// and comparison strings: > = <
// remember, a StringTokenizer will tokenize
// on any of the found delimiters
final StringTokenizer mTok = new StringTokenizer(controlString, "+-/*>=<\"");
while (mTok.hasMoreTokens())
{
String newString = mTok.nextToken();
String testString = newString;
boolean found = false;
// now Check for MIN or MAX
while (!found)
{
if (newString.indexOf("MAX") >= 0)
{
testString = newString.substring(0, newString.indexOf("MAX"));
newString = newString.substring(newString.indexOf("MAX") + 3);
}
else if (newString.indexOf("MIN") >= 0)
{
testString = newString.substring(0, newString.indexOf("MIN"));
newString = newString.substring(newString.indexOf("MIN") + 3);
}
else
{
found = true;
}
// check to see if it's a number
try
{
Float.parseFloat(testString);
}
catch (NumberFormatException e)
{
// It's a Variable!
if (!testString.isEmpty())
{
if (testString.startsWith("MOVE[")) {
testString =
new StringBuilder(testString.length())
.append("TYPE.")
.append(
testString.substring(5,
testString.length() - 1))
.toString();
}
dependMap.put(testString.intern(), "1");
addImpliedDependenciesFor(testString);
}
}
}
}
}
}
/**
* Add any dependencies implied by the provided dependency.
* @param aString The direct dependency being added.
*/
private void addImpliedDependenciesFor(String aString)
{
if (aString.indexOf("SKILLINFO(") >= 0)
{
dependMap.put("JEPFORMULA", "1");
}
if (aString.indexOf("HP") >= 0)
{
dependMap.put("CURRENTMAX", "1");
}
if (aString.indexOf("SKILL.") >= 0 || aString.indexOf("SKILLINFO") >= 0)
{
dependMap.put("NAME|STAT", "1");
}
if (aString.indexOf("MODEQUIPMAXDEX") >= 0)
{
dependMap.put("MAXDEX", "1");
}
if (aString.equals("BAB"))
{
dependMap.put("BASEAB", "1");
}
}
/**
* @see java.lang.Object#clone()
*/
@Override
public BonusObj clone() throws CloneNotSupportedException
{
final BonusObj bonusObj = (BonusObj) super.clone();
bonusObj.bonusInfo = new ArrayList<>(bonusInfo);
bonusObj.dependMap = new HashMap<>();
bonusObj.setValue(bonusFormula.toString());
// we want to keep the same references to these objects
// creatorObj
// targetObj
// These objects are immutable and do not need explicit cloning
// bonusName
// bonusType
// choiceString
// varPart
// isApplied
// valueIsStatic
// pcLevel
// typeOfBonus
return bonusObj;
}
public void setTokenSource(String tokenName)
{
tokenSource = tokenName;
}
public String getTokenSource()
{
return tokenSource;
}
@Override
public boolean equals(Object obj)
{
if (obj == this)
{
return true;
}
if (!(obj instanceof BonusObj))
{
return false;
}
BonusObj other = (BonusObj) obj;
return equalsPrereqObject(other)
&& bonusFormula.equals(other.bonusFormula)
&& bonusName.equals(other.bonusName)
&& bonusType.equals(other.bonusType)
&& theStackingFlag.equals(other.theStackingFlag)
&& bonusInfo.equals(other.bonusInfo);
}
@Override
public int hashCode()
{
// TODO Auto-generated method stub
return super.hashCode();
}
/*
* This makes an editor a bit more difficult, but since CHOOSE is an early
* target of 5.17, this probably isn't a big deal.
*/
private String originalString;
public void putOriginalString(String bonusString)
{
originalString = bonusString;
}
public String getLSTformat()
{
return originalString;
}
public String getDescription()
{
return getTypeOfBonus() + " " + getBonusInfo(); //$NON-NLS-1$
}
/**
* Identify if this bonus cannot have its target changed to upper case.
* @return true if the original case is needed for the targets.
*/
protected boolean requiresRealCaseTarget()
{
return false;
}
}