/*
* Initiative - A role playing utility to track turns
* Copyright (C) 2003 Devon Jones
*
* 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 May 24, 2003
*/
package plugin.initiative;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import pcgen.base.lang.StringUtil;
/**
* <p>Models an attack; that is a weapon or natural weapon with its
* attendant attack bonuses, damage, etc.</p>
* <p>This class is built around the values generated by the output
* tokens for weapons. It splits the to-hit values by slashes and
* represents them as a vector, counting all values after a ';' as
* off-hand attacks (as in +10/+5;+10). It splits damage into
* primary and off-hand by a slash, and the same with crit multiples
* and ranges.</p>
*
* @author Ross M. Lodge
*/
public class AttackModel extends PObjectModel
{
/** Constant for decoding incoming weapon strings */
private static final int SEGMENT_POSITION_TOHIT = 1;
/** Constant for decoding incoming weapon strings */
private static final int SEGMENT_POSITION_RANGE = 2;
/** Constant for decoding incoming weapon strings */
private static final int SEGMENT_POSITION_TYPE = 3;
/** Constant for decoding incoming weapon strings */
private static final int SEGMENT_POSITION_DAMAGE = 4;
/** Constant for decoding incoming weapon strings */
private static final int SEGMENT_POSITION_CRITRANGE = 5;
/** Constant for decoding incoming weapon strings */
private static final int SEGMENT_POSITION_CRITMULT = 6;
/** Constant for decoding incoming weapon strings */
private static final int SEGMENT_POSITION_HAND = 7;
/** Constant for decoding incoming weapon strings */
private static final int SEGMENT_POSITION_SIZE = 8;
/** Constant for decoding incoming weapon strings */
private static final int SEGMENT_POSITION_SPROP = 9;
/** Hand of weapon as string */
private String hand = null;
/** Range of weapon as string */
private String range = null;
/** Size of weapon as string. */
private String size = null;
/** Special properties of weapon as string. */
private String specialProp = null;
/** Type of weapon as string */
private String type = null;
/** List of crit multiples for weapon. */
private List<String> critMultiple = null;
/** List of critical ranges for weapon. */
private List<String> critRange = null;
/** List of damage strings for weapon. */
private List<String> damage = null;
/** List of to-hit bonuses. */
private List<String> toHit = null;
/** Index of first off-hand attack. */
private int firstOffHandAttack = -1;
/**
* <p>Constructs a new attack model based on a string. The string should
* have the following tokens, in the following order, separated by
* backslashes:</p>
* <ol>
* <li>|WEAPON.%weap.NAME|</li>
* <li>|WEAPON.%weap.TOTALHIT|</li>
* <li>|WEAPON.%weap.RANGE|</li>
* <li>|WEAPON.%weap.TYPE|</li>
* <li>|WEAPON.%weap.DAMAGE|</li>
* <li>|WEAPON.%weap.CRIT|</li>
* <li>|WEAPON.%weap.MULT|</li>
* <li>|WEAPON.%weap.HAND|</li>
* <li>|WEAPON.%weap.SIZE|</li>
* <li>|WEAPON.%weap.SPROP|</li>
* </ol>
*
* @param objectString
* The string to interpret as a weapon/attack.
*/
public AttackModel(String objectString)
{
super(objectString);
setToHit(getStringValue(outputTokens, SEGMENT_POSITION_TOHIT));
setRange(getStringValue(outputTokens, SEGMENT_POSITION_RANGE));
setType(getStringValue(outputTokens, SEGMENT_POSITION_TYPE));
setDamage(getStringValue(outputTokens, SEGMENT_POSITION_DAMAGE));
setCritRange(getStringValue(outputTokens, SEGMENT_POSITION_CRITRANGE));
setCritMultiple(getStringValue(outputTokens, SEGMENT_POSITION_CRITMULT));
setHand(getStringValue(outputTokens, SEGMENT_POSITION_HAND));
setSize(getStringValue(outputTokens, SEGMENT_POSITION_SIZE));
setSpecialProp(getStringValue(outputTokens, SEGMENT_POSITION_SPROP));
}
/**
* <p>Returns an integer array of bonuses as for a full attack, with the first attack (highest
* bonus) at index 0. If the toHit field is not set, returns an array with a single element
* with the bonuse of 0.</p>
*
* @return {@code int[]} containing the integer bonuses for a full attack action
*/
public int[] getBonusList()
{
int[] returnValue = null;
if ((toHit != null) && (!toHit.isEmpty()))
{
returnValue = new int[toHit.size()];
for (int i = 0; i < toHit.size(); i++)
{
returnValue[i] = getInt(toHit.get(i).substring(1));
}
}
return returnValue;
}
/**
* <p>
* Sets the crit multiple. This method splits the string before and after a
* slash, taking the value after the slash to indicate an off-hand attack
* (Head 2) multiple on a double weapon.
* </p>
*
* @param string
* The crit multiple string.
*/
public void setCritMultiple(String string)
{
if ((string != null) && (!string.isEmpty()))
{
StringTokenizer tok = new StringTokenizer(string, "/");
if (critMultiple == null)
{
critMultiple = new ArrayList<>(tok.countTokens());
}
else
{
critMultiple.clear();
}
while (tok.hasMoreTokens())
{
critMultiple.add(tok.nextToken());
}
}
else
{
if (critMultiple == null)
{
critMultiple = new ArrayList<>(1);
}
else
{
critMultiple.clear();
}
critMultiple.add("2");
}
}
/**
* <p>Assembles the crit multiple string and returns it</p>
*
* @return Crit multiple string
*/
public String getCritMultiple()
{
return StringUtil.join(critMultiple, "/");
}
/**
* <p>Gets the crit multiple at the specified attack bonus index, based
* on whether or not the index is greater than or less than the first off hand attack index.</p>
*
* @param index
* @return The crit multiple value.
*/
public String getCritMultiple(int index)
{
String returnValue = null;
int lookupAt = 0;
if ((firstOffHandAttack != -1) && (index >= firstOffHandAttack))
{
lookupAt = 1;
}
else
{
lookupAt = 0;
}
if ((critMultiple != null) && (critMultiple.size() > lookupAt))
{
returnValue = critMultiple.get(lookupAt);
}
else if ((critMultiple != null) && (!critMultiple.isEmpty()))
{
returnValue = critMultiple.get(0);
}
return returnValue;
}
/**
* <p>
* Sets the crit range before and after a slash, taking the values after
* the slash to indicate the bonuse for the off-hand (Head 2) of a double
* weapon.
* </p>
*
* @param string
* The crit range string
*/
public void setCritRange(String string)
{
if ((string != null) && (!string.isEmpty()))
{
StringTokenizer tok = new StringTokenizer(string, "/");
if (critRange == null)
{
critRange = new ArrayList<>(tok.countTokens());
}
else
{
critRange.clear();
}
while (tok.hasMoreTokens())
{
critRange.add(tok.nextToken());
}
}
else
{
if (critRange == null)
{
critRange = new ArrayList<>(1);
}
else
{
critRange.clear();
}
critRange.add("20");
}
}
/**
* <p>Gets the crit range value.</p>
*
* @return The crit range value.
*/
public String getCritRange()
{
return StringUtil.join(critRange, "/");
}
/**
* <p>Gets the crit range at the specified attack bonus index, based
* on whether or not the index is greater than or less than the first off hand attack index.</p>
*
* @param index The attack bonus index
* @return The requested crit range.
*/
public String getCritRange(int index)
{
String returnValue = null;
int lookupAt = 0;
if ((firstOffHandAttack != -1) && (index >= firstOffHandAttack))
{
lookupAt = 1;
}
else
{
lookupAt = 0;
}
if ((critRange != null) && (critRange.size() > lookupAt))
{
returnValue = critRange.get(lookupAt);
}
else if ((critRange != null) && (!critRange.isEmpty()))
{
returnValue = critRange.get(0);
}
return returnValue;
}
/**
* <p>Gets the minimum value of the crit range at the specified attack bonus
* index as an integer. So 19-20 would produce 19.</p>
*
* @param index
* The attack bonus index to retrieve values for.
* @return
* The minimum value for the crit range as an integer.
*/
public int getCritRangeMin(int index)
{
int returnValue;
String aRange =
new StringTokenizer(getCritRange(index), "-").nextToken();
returnValue = getInt(aRange);
return returnValue;
}
/**
* <p>
* Sets the damage string. This method splits the damage string around a
* slash, interpreting values after the slash to be the damage for the
* off-hand (head 2) of a double weapon.
* </p>
*
* @param string
* Damage string.
*/
public void setDamage(String string)
{
if ((string != null) && (!string.isEmpty()))
{
StringTokenizer tok = new StringTokenizer(string, "/");
if (damage == null)
{
damage = new ArrayList<>(tok.countTokens());
}
else
{
damage.clear();
}
while (tok.hasMoreTokens())
{
damage.add(tok.nextToken());
}
if (damage.size() > 1)
{
//If we've got a double weapon, pcgen is using AdB+C/+D, so
String damageDice = damage.get(0);
if (damageDice.lastIndexOf("+") > 0)
{
damageDice =
damageDice
.substring(0, damageDice.lastIndexOf("+"));
}
else if (damageDice.lastIndexOf("-") > 0)
{
damageDice =
damageDice
.substring(0, damageDice.lastIndexOf("-"));
}
for (int i = 1; i < damage.size(); i++)
{
String secondaryDamage = damage.get(i);
if (secondaryDamage.startsWith("+")
|| secondaryDamage.startsWith("-"))
{
damage.set(i, damageDice + secondaryDamage);
}
}
}
}
else
{
if (damage == null)
{
damage = new ArrayList<>(1);
}
else
{
damage.clear();
}
damage.add("1");
}
}
/**
* <p>Gets the damage string for the attack.</p>
* @return The damage string
*/
public String getDamage()
{
return StringUtil.join(damage, "/");
}
/**
* <p>Gets the damage dice at the specified attack bonus index, based
* on whether or not the index is greater than or less than the first off hand attack index.</p>
*
* @param index The attack bonus index
* @return The requested damage string.
*/
public String getDamage(int index)
{
String returnValue = null;
int lookupAt = 0;
if ((firstOffHandAttack != -1) && (index >= firstOffHandAttack))
{
lookupAt = 1;
}
else
{
lookupAt = 0;
}
if ((damage != null) && (damage.size() > lookupAt))
{
returnValue = damage.get(lookupAt);
}
else if ((damage != null) && (!damage.isEmpty()))
{
returnValue = damage.get(0);
}
return returnValue;
}
/**
* <p>Sets the hand value.</p>
* @param string
*/
public void setHand(String string)
{
hand = string;
}
/**
* <p>Gets the hand of the weapon as a string</p>
* @return The hand of the weapon
*/
public String getHand()
{
return hand;
}
/**
* <p>Sets the range value</p>
* @param string
*/
public void setRange(String string)
{
if ((string == null) || (!string.isEmpty()))
{
range = string;
}
else
{
range = "0'";
}
}
/**
* <p>Gets the range of the weapon as a string.</p>
* @return The range string.
*/
public String getRange()
{
return range;
}
/**
* <p>Gets the range of the weapon as an integer.</p>
* @return The range of the weapon as an integer.
*/
public int getRangeAsInt()
{
return getInt(range.replaceAll("\\D", ""));
}
/**
* <p>Sets the size value</p>
* @param string
*/
public void setSize(String string)
{
size = string;
}
/**
* <p>Gets the weapon size.</p>
* @return Size
*/
public String getSize()
{
return size;
}
/**
* <p>Sets the special property value.</p>
* @param string
*/
public void setSpecialProp(String string)
{
specialProp = string;
}
/**
* <p>Gets the special properties string.</p>
* @return Special properties.
*/
public String getSpecialProp()
{
return specialProp;
}
/**
* <p>
* Sets the two hit value. This method drives much of the functionality of
* the attack model. The string should be in the form <br>
* {@code
* Bonus/Bonus/Bonus...;off hand bonus/off hand bonus...
* }
* <br>
* The method splits the string into primary and off-hand bonus values, and
* splits the separate strings into individual bonuses. The length of the
* resulting list of bonuses drives the indices used to determine bonuses
* for each attack, and also which attacks are primary and off-hand.
* </p>
*
* @param string
* The attack bonuse string.
*/
public void setToHit(String string)
{
if (toHit == null)
{
toHit = new ArrayList<>();
}
else
{
toHit.clear();
}
firstOffHandAttack = -1;
StringTokenizer tok = new StringTokenizer(string, ";");
int handCount = 0;
int attackIndex = -1;
while (tok.hasMoreTokens())
{
handCount++;
StringTokenizer tok2 = new StringTokenizer(tok.nextToken(), "/");
while (tok2.hasMoreTokens())
{
attackIndex++;
if ((handCount == 2) && (firstOffHandAttack == -1))
{
firstOffHandAttack = attackIndex;
}
toHit.add(tok2.nextToken());
}
}
}
/**
* <p>Gets the to-hit string (like +10/+5;+8 . . .).</p>
* @return The to-hit string.
*/
public String getToHit()
{
StringBuilder sb = new StringBuilder();
if ((toHit != null) && (!toHit.isEmpty()))
{
for (int i = 0; i < toHit.size(); i++)
{
if (sb.length() > 0)
{
if (i == firstOffHandAttack)
{
sb.append(";");
}
else
{
sb.append("/");
}
}
sb.append(toHit.get(i));
}
}
return sb.toString();
}
/**
* <p>Gets the to-hit string at the specified attack index.</p>
*
* @param index
* @return The to-hit string.
*/
public String getToHit(int index)
{
String returnValue = null;
if ((toHit != null) && (toHit.size() > index))
{
returnValue = toHit.get(index);
}
return returnValue;
}
/**
* <p>Sets teh weapon type.</p>
* @param string
*/
public void setType(String string)
{
type = string;
}
/**
* <p>Gets the weapon type.</p>
* @return The weapon type
*/
public String getType()
{
return type;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*
* Gets a string representation of the object.
*/
@Override
public String toString()
{
String returnValue;
returnValue =
getName() + " " + getToHit() + " " + getRange() + "/"
+ getType() + " (" + getDamage() + " " + getCritRange()
+ "/x" + getCritMultiple() + " " + getHand() + " "
+ getSize()
+ ("".equals(getSpecialProp()) ? "" : getSpecialProp())
+ ")";
return returnValue;
}
}