/* * SkillToken.java * Copyright 2004 (C) James Dempsey <jdempsey@users.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 * * Created on Aug 5, 2004 * * $Id$ * */ package pcgen.io.exporttoken; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; import org.apache.commons.lang3.StringUtils; import pcgen.cdom.enumeration.ObjectKey; import pcgen.cdom.enumeration.SkillCost; import pcgen.cdom.enumeration.SkillFilter; import pcgen.core.Globals; import pcgen.core.PCClass; import pcgen.core.PlayerCharacter; import pcgen.core.SettingsHandler; import pcgen.core.Skill; import pcgen.core.analysis.QualifiedName; import pcgen.core.analysis.SkillInfoUtilities; import pcgen.core.analysis.SkillModifier; import pcgen.core.analysis.SkillRankControl; import pcgen.core.display.SkillCostDisplay; import pcgen.core.display.SkillDisplay; import pcgen.io.ExportHandler; import pcgen.util.Logging; import pcgen.util.enumeration.View; /** * {@code SkillToken} is the base class for the SKILL * family of tokens. It also handles the processing of the SKILL * token itself, which outputs select information about a * choosen skill. The format for this tag is SKILL.id.property * where id cna be either an index or a skill name and the * property is optional. eg SKILL.2.RANK or SKILL.BALANCE * * * @author James Dempsey <jdempsey@users.sourceforge.net> */ public class SkillToken extends Token { /** Token name */ public static final String TOKENNAME = "SKILL"; // Constants for the property to be output. public static final int SKILL_NAME = 0; public static final int SKILL_TOTAL = 1; public static final int SKILL_RANK = 2; public static final int SKILL_MOD = 3; public static final int SKILL_ABILITY = 4; public static final int SKILL_ABMOD = 5; public static final int SKILL_MISC = 6; public static final int SKILL_UNTRAINED = 7; public static final int SKILL_EXCLUSIVE = 8; public static final int SKILL_UNTRAINED_EXTENDED = 9; public static final int SKILL_ACP = 10; public static final int SKILL_EXCLUSIVE_TOTAL = 11; public static final int SKILL_TRAINED_TOTAL = 12; public static final int SKILL_EXPLANATION = 13; public static final int SKILL_TYPE = 14; public static final int SKILL_COST = 15; public static final int SKILL_SIZE= 16; public static final int SKILL_CLASSES= 17; // Cache the skill list as it is expensive to build private List<Skill> cachedSkillList = null; private PlayerCharacter lastPC = null; private int lastPCSerial; /** * @see pcgen.io.exporttoken.Token#getTokenName() */ @Override public String getTokenName() { return TOKENNAME; } /** * @see pcgen.io.exporttoken.Token#getToken(java.lang.String, pcgen.core.PlayerCharacter, pcgen.io.ExportHandler) */ @Override public String getToken(String tokenSource, PlayerCharacter pc, ExportHandler eh) { SkillDetails details = buildSkillDetails(tokenSource); Skill aSkill = getSkill(pc, details, eh); return getSkillProperty(aSkill, details.getProperty(0), pc); } /** * Select the target skill based on the supplied critieria. Uses the * id in the details object to either retrieve a skill by name or by * position in the skill list. * * @param pc The character being processed. * @param details The parsed details of the token. * @param eh The ExportHandler * @return The matching skill, or null if none match. */ private Skill getSkill(PlayerCharacter pc, SkillDetails details, ExportHandler eh) { Skill skill = null; try { final int i = Integer.parseInt(details.getSkillId()); final List<Skill> pcSkills = new ArrayList<>(getSkillList(pc)); SkillFilter filter = details.getSkillFilter(); if (filter == null || filter == SkillFilter.Selected) { filter = pc.getSkillFilter(); } Iterator<Skill> iter = pcSkills.iterator(); while (iter.hasNext()) { Skill sk = iter.next(); if (!pc.includeSkill(sk, filter) || !sk.qualifies(pc, null)) { iter.remove(); } } if ((i >= (pcSkills.size() - 1)) && eh != null && eh.getExistsOnly()) { eh.setNoMoreItems(true); } if (i < pcSkills.size()) { skill = pcSkills.get(i); } } catch (NumberFormatException exc) { //Allowing SKILL.Spot.<subtoken> skill = Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject( Skill.class, details.getSkillId()); } return skill; } private synchronized List<Skill> getSkillList(PlayerCharacter pc) { if (pc == lastPC && pc.getSerial() == lastPCSerial) { return cachedSkillList; } final List<Skill> pcSkills = SkillDisplay.getSkillListInOutputOrder(pc, pc.getDisplay() .getPartialSkillList(View.VISIBLE_EXPORT)); cachedSkillList = pcSkills; lastPC = pc; lastPCSerial = pc.getSerial(); return pcSkills; } /** * Given the source of the token, split it up into its skill id and * properties. The token itself is ignopred as this has already been * processed elsewhere. The expected format is token.skillid.property... * * @param tokenSource The source of the token. * @return A SkillDetails containing the details of the token. */ public static SkillDetails buildSkillDetails(String tokenSource) { final StringTokenizer aTok = new StringTokenizer(tokenSource, "."); SkillFilter filter = null; List<String> properties = new ArrayList<>(); // Split out the parts of the source String skillId = ""; for (int i = 0; aTok.hasMoreTokens(); ++i) { String token = aTok.nextToken(); if (i == 0) { // Ignore } else if (i == 1) { skillId = token; } else { filter = SkillFilter.getByToken(token); if (filter != null) { if (aTok.hasMoreTokens()) { token = aTok.nextToken(); } else { token = "NAME"; } } properties.add(token); } } // Create and return the SkillDetails object. return new SkillDetails(skillId, properties, filter); } /** * Calculate the value of the specified skill property for the * supplied skill and character. * * @param aSkill The skill to be processed. * @param property The property being processed. * @param pc The character to be reported. * @return The skill tag output value. */ protected String getSkillProperty(Skill aSkill, String property, PlayerCharacter pc) { if (aSkill == null) { return ""; } int action = getPropertyId(property); return getSkillPropValue(aSkill, action, property, pc); } /** * Convert a property name into the id of the property. * * @param property The property name. * @return The id of the property. */ public static int getPropertyId(String property) { int propId = 0; if ("NAME".equalsIgnoreCase(property)) { propId= SKILL_NAME; } else if ("TOTAL".equalsIgnoreCase(property)) { propId = SKILL_TOTAL; } else if ("RANK".equalsIgnoreCase(property)) { propId= SKILL_RANK; } else if ("MOD".equalsIgnoreCase(property)) { propId= SKILL_MOD; } else if ("ABILITY".equalsIgnoreCase(property)) { propId= SKILL_ABILITY; } else if ("ABMOD".equalsIgnoreCase(property)) { propId= SKILL_ABMOD; } else if ("MISC".equalsIgnoreCase(property)) { propId= SKILL_MISC; } else if ("COST".equalsIgnoreCase(property)) { propId= SKILL_COST; } else if ("UNTRAINED".equalsIgnoreCase(property)) { propId= SKILL_UNTRAINED; } else if ("EXCLUSIVE".equalsIgnoreCase(property)) { propId= SKILL_EXCLUSIVE; } else if (property.regionMatches(true, 0, "UNTRAINED", 0, 9)) { propId= SKILL_UNTRAINED_EXTENDED; } else if (property.regionMatches(true, 0, "ACP", 0, 3)) { propId= SKILL_ACP; } else if ("EXCLUSIVE_TOTAL".equalsIgnoreCase(property)) { propId= SKILL_EXCLUSIVE_TOTAL; } else if ("TRAINED_TOTAL".equalsIgnoreCase(property)) { propId= SKILL_TRAINED_TOTAL; } else if (property.regionMatches(true, 0, "EXPLAIN", 0, 7)) { propId= SKILL_EXPLANATION; } else if ("TYPE".equalsIgnoreCase(property)) { propId= SKILL_TYPE; } else if ("SIZE".equalsIgnoreCase(property)) { propId= SKILL_SIZE; } else if ("CLASSES".equalsIgnoreCase(property)) { propId= SKILL_CLASSES; } return propId; } /** * Evaluate the property for the supplied skill and character. For * properties such as ACP and the extended UNTRAINED property, the * property text is required to be further parsed to pull out user * defined text to be output in each case. * * @param aSkill The skill to be reported upon. * @param property The property to be reported. * @param propertyText The orginal text of the property. * @param pc The character to be reported upon. * @return The value of the property. */ private String getSkillPropValue(Skill aSkill, int property, String propertyText, PlayerCharacter pc) { StringBuilder retValue = new StringBuilder(); if (((property == SKILL_ABMOD) || (property == SKILL_MISC)) && false)//&& aSkill.get(ObjectKey.KEY_STAT) == null) { retValue.append("n/a"); } else { switch (property) { case SKILL_NAME: retValue.append(QualifiedName.qualifiedName(pc, aSkill)); break; case SKILL_TOTAL: if (SettingsHandler.getGame().hasSkillRankDisplayText()) { retValue.append(SettingsHandler.getGame() .getSkillRankDisplayText( SkillRankControl.getTotalRank(pc, aSkill).intValue() + SkillModifier.modifier(aSkill, pc).intValue())); } else { retValue.append(Integer.toString(SkillRankControl.getTotalRank(pc, aSkill).intValue() + SkillModifier.modifier(aSkill, pc).intValue())); } break; case SKILL_RANK: if (SettingsHandler.getGame().hasSkillRankDisplayText()) { retValue.append(SettingsHandler.getGame() .getSkillRankDisplayText( SkillRankControl.getTotalRank(pc, aSkill).intValue())); } else { retValue.append(SkillRankControl.getTotalRank(pc, aSkill).toString()); } break; case SKILL_MOD: retValue.append(SkillModifier.modifier(aSkill, pc).toString()); break; case SKILL_ABILITY: retValue.append(SkillInfoUtilities.getKeyStatFromStats(pc, aSkill)); break; case SKILL_ABMOD: retValue.append(Integer.toString(SkillModifier.getStatMod(aSkill, pc))); break; case SKILL_MISC: retValue.append(Integer.toString(SkillModifier.modifier(aSkill, pc) .intValue() - SkillModifier.getStatMod(aSkill, pc))); break; case SKILL_UNTRAINED: retValue.append(aSkill.getSafe(ObjectKey.USE_UNTRAINED) ? "Y" : "NO"); break; case SKILL_EXCLUSIVE: retValue.append(aSkill.getSafe(ObjectKey.EXCLUSIVE) ? "Y" : "N"); break; case SKILL_UNTRAINED_EXTENDED: retValue.append(getUntrainedOutput(aSkill, propertyText)); break; case SKILL_ACP: retValue.append(getAcpOutput(aSkill, propertyText)); break; case SKILL_COST: SkillCost cost = null; for (PCClass pcc : pc.getDisplay().getClassSet()) { if (cost == null) { cost = pc.getSkillCostForClass(aSkill, pcc); } else { SkillCost newCost = pc.getSkillCostForClass(aSkill, pcc); if (SkillCost.CLASS.equals(newCost) || SkillCost.EXCLUSIVE.equals(cost)) { cost = newCost; } } if (SkillCost.CLASS.equals(cost)) { break; } } retValue.append(cost.toString()); break; case SKILL_EXCLUSIVE_TOTAL: retValue .append(Integer .toString(((aSkill.getSafe(ObjectKey.EXCLUSIVE) || !aSkill.getSafe(ObjectKey.USE_UNTRAINED)) && (SkillRankControl.getTotalRank(pc, aSkill) .intValue() == 0)) ? 0 : (SkillRankControl.getTotalRank(pc, aSkill).intValue() + SkillModifier.modifier(aSkill, pc).intValue()))); break; case SKILL_TRAINED_TOTAL: retValue.append(Integer .toString((!aSkill.getSafe(ObjectKey.USE_UNTRAINED) && (SkillRankControl.getTotalRank(pc, aSkill).intValue() == 0)) ? 0 : (SkillRankControl.getTotalRank(pc, aSkill).intValue() + SkillModifier.modifier(aSkill, pc) .intValue()))); break; case SKILL_EXPLANATION: boolean shortFrom = !("_LONG".equals(propertyText.substring(7))); String bonusDetails = SkillCostDisplay.getModifierExplanation(aSkill, pc, shortFrom); retValue.append(bonusDetails); break; case SKILL_TYPE: String type = aSkill.getType(); retValue.append(type); break; case SKILL_SIZE: retValue.append(Integer.toString((int)(pc.getSizeAdjustmentBonusTo("SKILL", aSkill.getKeyName())))); break; case SKILL_CLASSES: List<String> classes = new ArrayList<>(); for (PCClass aClass : pc.getClassList()) { if (pc.getSkillCostForClass(aSkill, aClass) == SkillCost.CLASS) { classes.add(aClass.getDisplayName()); } } retValue.append(StringUtils.join(classes, ".")); break; default: Logging .errorPrint("In ExportHandler._writeSkillProperty the propIdvalue " + property + " is not handled."); break; } } return retValue.toString(); } /** * Process the untrained tag. * Syntax: SKILL.%.UNTRAINEDfoo,bar * where foo and bar are optional strings of unfixed length. * Behavior: prints out foo if the skill is usable untrained, * bar if not usable untrained. * if bar is not supplied, nothing is printed if untrained. If neither foo * nor bar are supplied, why are you using this tag? * * @param aSkill The skill to be processed. * @param property The property * @return The string to be output. */ public static String getUntrainedOutput(Skill aSkill, String property) { StringTokenizer aTok = new StringTokenizer(property.substring(9), ","); String untrained_tok; String trained_tok; if (aTok.hasMoreTokens()) { untrained_tok = aTok.nextToken(); } else { untrained_tok = ""; } if (aTok.hasMoreTokens()) { trained_tok = aTok.nextToken(); } else { trained_tok = ""; } if (aSkill.getSafe(ObjectKey.USE_UNTRAINED)) { return untrained_tok; } return trained_tok; } /** * Process the Armour Check Penalty tag. * Syntax: SKILL.%.ACPfoo,bar,baz,bot * where foo, bar, baz, and bot are strings of unfixed length. * Behavior: tests for armor check penalty interaction with this skill. * foo is printed if the skill is not affected by ACP. * bar is printed if the skill is affected by ACP. * baz is printed if the skill is only affected by ACP if the user * is untrained * bot is printed if the skill has the special weight penalty * (like Swim) * * @param aSkill The skill instance to be processed * @param property The output property supplied. * @return The ACP tag output. */ public static String getAcpOutput(Skill aSkill, String property) { final StringTokenizer aTok = new StringTokenizer(property.substring(3), ","); int numArgs = aTok.countTokens(); int acp = aSkill.getSafe(ObjectKey.ARMOR_CHECK).ordinal(); String acpText[] = new String[numArgs]; for (int i = 0; aTok.hasMoreTokens(); i++) { acpText[i] = aTok.nextToken(); } return ((acp < numArgs) && (acp >= 0)) ? acpText[acp] : ""; } // ================== Inner class ======================= /** * {@code SkillDetails} holds the parsed details of a skill * token. Note that apart from updating the properties array contents, * instances of this class are immutable. * */ public final static class SkillDetails { /** The id of the skill - normally an index or a skill name. */ private final String skillId; /** The list of properties for the token. */ private final List<String> properties; /** The skilll list filter */ private final SkillFilter filter; /** * Constructor for skill details. Creates an immutable instance * with the specified id and properties list. * * @param inSkillId The id of the skill - normally an index or skill name. * @param inProperties The loist of properties, can be types, prefixes * and properties to be displayed. */ SkillDetails(String inSkillId, List<String> inProperties, SkillFilter inFilter) { this.skillId = inSkillId; this.properties = inProperties; this.filter = inFilter; } public int getPropertyCount() { return properties.size(); } public String getProperty(int index) { if (index < properties.size()) { return properties.get(index); } else { return ""; } } /** * Get the ID of the Skill * @return the ID of the Skill */ public String getSkillId() { return skillId; } /** * Get the skill filter * @return the skill filter */ public SkillFilter getSkillFilter() { return filter; } } }