/* * PCClass.java * Copyright 2001 (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 April 21, 2001, 2:15 PM * * $Id$ */ package pcgen.core; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.StringTokenizer; import java.util.TreeMap; import org.apache.commons.lang3.StringUtils; import pcgen.base.lang.StringUtil; import pcgen.cdom.base.AssociatedPrereqObject; import pcgen.cdom.base.CDOMListObject; import pcgen.cdom.base.CDOMObject; import pcgen.cdom.base.CDOMObjectUtilities; import pcgen.cdom.base.CDOMReference; import pcgen.cdom.base.Constants; import pcgen.cdom.base.TransitionChoice; import pcgen.cdom.content.HitDie; import pcgen.cdom.content.KnownSpellIdentifier; import pcgen.cdom.content.LevelCommandFactory; import pcgen.cdom.enumeration.FactKey; import pcgen.cdom.enumeration.IntegerKey; import pcgen.cdom.enumeration.ListKey; import pcgen.cdom.enumeration.MapKey; import pcgen.cdom.enumeration.ObjectKey; import pcgen.cdom.enumeration.Region; import pcgen.cdom.enumeration.StringKey; import pcgen.cdom.enumeration.Type; import pcgen.cdom.enumeration.VariableKey; import pcgen.cdom.helper.ArmorProfProvider; import pcgen.cdom.helper.ClassSource; import pcgen.cdom.helper.ShieldProfProvider; import pcgen.cdom.helper.WeaponProfProvider; import pcgen.cdom.inst.PCClassLevel; import pcgen.cdom.list.DomainList; import pcgen.cdom.reference.CDOMDirectSingleRef; import pcgen.cdom.reference.CDOMSingleRef; import pcgen.core.analysis.AddObjectActions; import pcgen.core.analysis.DomainApplication; import pcgen.core.analysis.ExchangeLevelApplication; import pcgen.core.analysis.SizeUtilities; import pcgen.core.analysis.SkillRankControl; import pcgen.core.analysis.StatApplication; import pcgen.core.analysis.SubClassApplication; import pcgen.core.analysis.SubstitutionClassApplication; import pcgen.core.bonus.Bonus; import pcgen.core.bonus.BonusObj; import pcgen.facade.core.ClassFacade; import pcgen.core.pclevelinfo.PCLevelInfo; import pcgen.core.pclevelinfo.PCLevelInfoStat; import pcgen.core.prereq.PrereqHandler; import pcgen.core.prereq.Prerequisite; import pcgen.core.spell.Spell; import pcgen.core.utils.MessageType; import pcgen.core.utils.ShowMessageDelegate; import pcgen.persistence.PersistenceLayerException; import pcgen.persistence.lst.output.prereq.PrerequisiteWriter; import pcgen.persistence.lst.prereq.PreParserFactory; import pcgen.rules.context.AbstractReferenceContext; import pcgen.util.Logging; import pcgen.util.enumeration.AttackType; /** * {@code PCClass}. * * @author Bryan McRoberts <merton_monk@users.sourceforge.net> */ public class PCClass extends PObject implements ClassFacade { public static final CDOMReference<DomainList> ALLOWED_DOMAINS; static { DomainList dl = new DomainList(); dl.setName("*Allowed"); ALLOWED_DOMAINS = CDOMDirectSingleRef.getRef(dl); } /* * TYPESAFETY This is definitely something that needs to NOT be a String, * but it gets VERY complicated to do that, since the keys are widely used * in the variable processor. */ /* * ALLCLASSLEVELS Must be in all PCClassLevels, since this is the master * index of what this PCClassLevel was created from. Best for debugging, and * not necessarily creation on the fly? */ /* * REFACTOR This brings up a FASCINATING point, in that the Class Key today * is used in variable processing. How is that to be handled going forward - * so that PCClassLevels are actually what is checked and NOT the PCClass? * What needs to be done to teach the system to iterate over all * PCClassLevels that implement this key? */ private String classKey = null; /** * Default Constructor. Constructs an empty PCClass. */ public PCClass() { super(); } /** * Returns the abbreviation for this class. * * @return The abbreviation string. */ /* * FINALPCCLASSANDLEVEL This is required in PCClassLevel and should be present in * PCClass for PCClassLevel creation (in the factory) */ @Override public final String getAbbrev() { FactKey<String> fk = FactKey.valueOf("Abb"); String abb = getResolved(fk); if (abb == null) { String name = getDisplayName(); abb = name.substring(0, Math.min(3, name.length())); } return abb; } /** * Return the qualified key, usually used as the source in a * getVariableValue call. Overriden here to return CLASS:keyname * * @return The qualified key of the object */ /* * PCCLASSANDLEVEL Since the classKey is generally the universal index of whether * two PCClassLevels are off of the same base, classKey will be populated into * each PCClassLevel. This method must therefore also be in both PCClass and * PCClassLevel */ @Override public String getQualifiedKey() { if (classKey == null) { classKey = "CLASS:" + getKeyName(); //$NON-NLS-1$ } return classKey; } /** * Returns the total bonus to the specified bonus type and name. * * <p> * This method checks only bonuses associated with the class. It makes sure * to return bonuses that are active only to the max level specified. What * that means is that bonuses specified on class level lines will have a * level parameter associated with them. Only bonuses specified on the level * specified or lower will be totalled. * * @param argType * Bonus type e.g. <code>BONUS:<b>DOMAIN</b></code> * @param argMname * Bonus name e.g. <code>BONUS:DOMAIN|<b>NUMBER</b></code> * @param asLevel * The maximum level to apply bonuses for. * @param aPC * The <tt>PlayerCharacter</tt> bonuses are being calculated * for. * * @return Total bonus value. */ /* * REFACTOR There is potentially redundant information here - level and PC... * is this ever out of sync or can this method be removed/made private?? */ public double getBonusTo(final String argType, final String argMname, final int asLevel, final PlayerCharacter aPC) { double i = 0; List<BonusObj> rawBonusList = getRawBonusList(aPC); for (int lvl = 1 ; lvl < asLevel; lvl++) { rawBonusList.addAll(aPC.getActiveClassLevel(this, lvl).getRawBonusList(aPC)); } if ((asLevel == 0) || rawBonusList.isEmpty()) { return 0; } final String type = argType.toUpperCase(); final String mname = argMname.toUpperCase(); for (final BonusObj bonus : rawBonusList) { final StringTokenizer breakOnPipes = new StringTokenizer(bonus.toString().toUpperCase(), Constants.PIPE, false); final String theType = breakOnPipes.nextToken(); if (!theType.equals(type)) { continue; } final String str = breakOnPipes.nextToken(); final StringTokenizer breakOnCommas = new StringTokenizer(str, Constants.COMMA, false); while (breakOnCommas.hasMoreTokens()) { final String theName = breakOnCommas.nextToken(); if (theName.equals(mname)) { final String aString = breakOnPipes.nextToken(); final List<Prerequisite> localPreReqList = new ArrayList<>(); if (bonus.hasPrerequisites()) { localPreReqList.addAll(bonus.getPrerequisiteList()); } // TODO: This code should be removed after the 5.8 release // as the prereqs are processed by the bonus loading code. while (breakOnPipes.hasMoreTokens()) { final String bString = breakOnPipes.nextToken(); if (PreParserFactory.isPreReqString(bString)) { Logging .debugPrint("Why is this prerequisite '" + bString + "' parsed in '" + getClass().getName() + ".getBonusTo(String,String,int)' rather than in the persistence layer?"); //$NON-NLS-1$ //$NON-NLS-2$//$NON-NLS-3$ try { final PreParserFactory factory = PreParserFactory.getInstance(); localPreReqList.add(factory.parse(bString)); } catch (PersistenceLayerException ple) { Logging.errorPrint(ple.getMessage(), ple); } } } // must meet criteria for bonuses before adding them in // TODO: This is a hack to avoid VARs etc in class defs // being qualified for when Bypass class prereqs is // selected. // Should we be passing in the BonusObj here to allow it to // be referenced in Qualifies statements? if (PrereqHandler.passesAll(localPreReqList, aPC, null)) { final double j = aPC.getVariableValue(aString, getQualifiedKey()) .doubleValue(); i += j; } } } } return i; } /** * Identify if this class has a cap on the number of levels it is * possible to take. * @return true if a cap on levels exists, false otherwise. */ public final boolean hasMaxLevel() { Integer ll = get(IntegerKey.LEVEL_LIMIT); return ll != null && ll != Constants.NO_LEVEL_LIMIT; } /* * REFACTOR This is BAD that this is referring to PCLevelInfo - that gets * VERY confusing as far as object interaction. Can we get rid of * PCLevelInfo altogether? */ public final int getSkillPool(final PlayerCharacter aPC) { int returnValue = 0; // ////////////////////////////////// // Using this method will return skills for level 0 even when there is // no information // Byngl - December 28, 2004 // for (int i = 0; i <= level; i++) // { // final PCLevelInfo pcl = aPC.getLevelInfoFor(getKeyName(), i); // // if ((pcl != null) && pcl.getClassKeyName().equals(getKeyName())) // { // returnValue += pcl.getSkillPointsRemaining(); // } // } for (PCLevelInfo pcl : aPC.getLevelInfo()) { if (pcl.getClassKeyName().equals(getKeyName())) { returnValue += pcl.getSkillPointsRemaining(); } } // ////////////////////////////////// return returnValue; } /* * FINALPCCLASSANDLEVEL This is required in PCClassLevel and should be present in * PCClass for PCClassLevel creation (in the factory) */ public final String getSpellBaseStat() { Boolean useStat = get(ObjectKey.USE_SPELL_SPELL_STAT); if (useStat == null) { return "None"; } else if (useStat) { return "SPELL"; } Boolean otherCaster = get(ObjectKey.CASTER_WITHOUT_SPELL_STAT); if (otherCaster) { return "OTHER"; } CDOMSingleRef<PCStat> ss = get(ObjectKey.SPELL_STAT); //TODO This could be null, do we need to worry about it? return ss.get().getKeyName(); } /* * FINALPCCLASSANDLEVEL This is required in PCClassLevel and should be present in * PCClass for PCClassLevel creation (in the factory) */ @Override public final String getSpellType() { FactKey<String> fk = FactKey.valueOf("SpellType"); String castInfo = getResolved(fk); return StringUtils.isEmpty(castInfo) ? Constants.NONE : castInfo; } /* * FINALPCCLASSONLY This is really an item that the PCClass knows, and then the * selected subClass, if any, is structured into the PCClassLevel during the * construction of the PCClassLevel */ public final SubClass getSubClassKeyed(final String aKey) { List<SubClass> subClassList = getListFor(ListKey.SUB_CLASS); if (subClassList == null) { return null; } for (SubClass subClass : subClassList) { if (subClass.getKeyName().equals(aKey)) { return subClass; } } return null; } /* * FINALPCCLASSONLY This is really an item that the PCClass knows, and then the * selected substitutionClass, if any, is structured into the PCClassLevel * during the construction of the PCClassLevel */ public final SubstitutionClass getSubstitutionClassKeyed(final String aKey) { List<SubstitutionClass> substitutionClassList = getListFor(ListKey.SUBSTITUTION_CLASS); if (substitutionClassList == null) { return null; } for (SubstitutionClass sc : substitutionClassList) { if (sc.getKeyName().equals(aKey)) { return sc; } } return null; } public void setLevel(final int newLevel, final PlayerCharacter aPC) { final int curLevel = aPC.getLevel(this); if (newLevel >= 0) { aPC.setLevelWithoutConsequence(this, newLevel); } if (newLevel == 1) { if (newLevel > curLevel || aPC.isImporting()) { addFeatPoolBonus(aPC); } } if (!aPC.isImporting()) { aPC.calcActiveBonuses(); //Need to do this again if caching is re-integrated //aPC.getSpellTracker().buildSpellLevelMap(newLevel); } if ((newLevel == 1) && !aPC.isImporting() && (curLevel == 0)) { SubClassApplication.checkForSubClass(aPC, this); aPC.setSpellLists(this); } if (!aPC.isImporting() && (curLevel < newLevel)) { SubstitutionClassApplication.checkForSubstitutionClass(this, newLevel, aPC); } for (PCClass pcClass : aPC.getClassSet()) { aPC.calculateKnownSpellsForClassLevel(this); } } /** * Add the bonus to the character's feat pool that is granted by the class. * NB: LEVELSPERFEAT is now handled via PLayerCHaracter.getNumFeatsFromLevels() * rather than bonuses. Only the standard feat progression for the gamemode is * handled here. * @param aPC The character to bonus. */ void addFeatPoolBonus(final PlayerCharacter aPC) { Integer mLevPerFeat = get(IntegerKey.LEVELS_PER_FEAT); int startLevel; int rangeLevel; int divisor; if (mLevPerFeat == null) { String aString = Globals.getBonusFeatString(); StringTokenizer aTok = new StringTokenizer(aString, "|", false); startLevel = Integer.parseInt(aTok.nextToken()); rangeLevel = Integer.parseInt(aTok.nextToken()); divisor = rangeLevel; if (divisor > 0) { StringBuilder aBuf = new StringBuilder("FEAT|PCPOOL|") .append("max(CL"); // Make sure we only take off the startlevel value once if (this == aPC.getClassKeyed(aPC.getLevelInfoClassKeyName(0))) { aBuf.append("-").append(startLevel); aBuf.append("+").append(rangeLevel); } aBuf.append(",0)/").append(divisor); // Logging.debugPrint("Feat bonus for " + this + " is " // + aBuf.toString()); BonusObj bon = Bonus.newBonus(Globals.getContext(), aBuf.toString()); aPC.addBonus(bon, this); } } } /* * FINALPCCLASSANDLEVEL This is required in PCClassLevel and should be present in * PCClass for PCClassLevel creation (in the factory) */ /* * FUTUREREFACTOR This would really be nice to have initilized when the LST files * are read in, which is possible because the ClassTypes are all defined as part * of the GameMode... however the problem is that the order of the ISMONSTER tag * and the TYPE tags cannot be defined - .MODs and .COPYs make it impossible to * guarantee an order. Therefore, this must wait for a two-pass design in the * import system - thpr 10/4/06 */ public boolean isMonster() { Boolean mon = get(ObjectKey.IS_MONSTER); if (mon != null) { return mon.booleanValue(); } ClassType aClassType = SettingsHandler.getGame().getClassTypeByName(getClassType()); if ((aClassType != null) && aClassType.isMonster()) { return true; } else { for (Type type : getTrueTypeList(false)) { aClassType = SettingsHandler.getGame().getClassTypeByName(type.toString()); if ((aClassType != null) && aClassType.isMonster()) { return true; } } } return false; } @Override public String getPCCText() { final StringBuilder pccTxt = new StringBuilder(200); pccTxt.append("CLASS:").append(getDisplayName()); pccTxt.append("\t"); pccTxt.append(PrerequisiteWriter.prereqsToString(this)); pccTxt.append("\t"); pccTxt.append(StringUtil.joinToStringBuilder(Globals.getContext().unparse( this), "\t")); // now all the level-based stuff final String lineSep = System.getProperty("line.separator"); for (Map.Entry<Integer, PCClassLevel> me : levelMap.entrySet()) { pccTxt.append(lineSep).append(me.getKey()).append('\t'); pccTxt.append(PrerequisiteWriter.prereqsToString(me.getValue())); pccTxt.append("\t"); pccTxt.append(StringUtil.joinToStringBuilder(Globals.getContext() .unparse(me.getValue()), "\t")); } return pccTxt.toString(); } /* * FINALPCCLASSONLY This is really an item that the PCClass knows, and then the * selected subClass, if any, is structured into the PCClassLevel during the * construction of the PCClassLevel */ public final void addSubClass(final SubClass sClass) { sClass.put(ObjectKey.LEVEL_HITDIE, get(ObjectKey.LEVEL_HITDIE)); addToListFor(ListKey.SUB_CLASS, sClass); } /* * PCCLASSONLY This is really an item that the PCClass knows, and then the * selected substitutionClass, if any, is structured into the PCClassLevel * during the construction of the PCClassLevel */ public final void addSubstitutionClass(final SubstitutionClass sClass) { sClass.put(ObjectKey.LEVEL_HITDIE, get(ObjectKey.LEVEL_HITDIE)); addToListFor(ListKey.SUBSTITUTION_CLASS, sClass); } /** * returns the value at which another attack is gained attackCycle of 4 * means a second attack is gained at a BAB of +5/+1 * * @param at the AttackType * @return int */ /* * PCCLASSANDLEVEL Some derivative of this will likely need to exist in both * PCClass (since it's a tag) and PCClassLevel (since there will have to be * some method of detecting what the BAB of a given PCClassLevel is and then * grouping those in the proper groups (see * PlayerCharacter.getAttackString()) to determine what the final attack * bonuses are. */ public int attackCycle(final AttackType at) { for (Map.Entry<AttackType, Integer> me : getMapFor(MapKey.ATTACK_CYCLE) .entrySet()) { if (at.equals(me.getKey())) { return me.getValue(); } } return SettingsHandler.getGame().getBabAttCyc(); } public int baseAttackBonus(final PlayerCharacter aPC) { if (aPC.getLevel(this) == 0) { return 0; } return (int) getBonusTo("COMBAT", "BASEAB", aPC.getLevel(this), aPC); } /** * -2 means that the spell itself indicates what stat should be used, * otherwise this method returns an index into the global list of stats for * which stat the bonus spells are based upon. * * @return int Index of the class' spell stat, or -2 if spell based */ /* * REFACTOR Why is this returning an INT and not a PCStat or something like * that? or why is the user not just using getSpellBaseStat and processing * the response by itself?? */ public PCStat baseSpellStat() { if (getSafe(ObjectKey.USE_SPELL_SPELL_STAT)) { return null; } if (getSafe(ObjectKey.CASTER_WITHOUT_SPELL_STAT)) { return null; } CDOMSingleRef<PCStat> ss = get(ObjectKey.SPELL_STAT); if (ss != null) { return ss.get(); } if (Logging.isDebugMode()) { Logging.debugPrint("Found Class: " + getDisplayName() + " that did not have any SPELLSTAT defined"); } return null; } /** * Returns the stat to use for bonus spells. * * <p> * The method checks to see if a BONUSSPELLSTAT: has been set for the class. * If it is set to a stat that stat is returned. If it is set to None null is * returned. If it is set to Default then the BASESPELLSTAT is returned. * * @return the stat to use for bonus spells. */ public PCStat bonusSpellStat() { Boolean hbss = get(ObjectKey.HAS_BONUS_SPELL_STAT); if (hbss == null) { return baseSpellStat(); } else if (hbss) { CDOMSingleRef<PCStat> bssref = get(ObjectKey.BONUS_SPELL_STAT); if (bssref != null) { return bssref.get(); } } return null; } @Override public PCClass clone() { PCClass aClass = null; try { aClass = (PCClass) super.clone(); List<KnownSpellIdentifier> ksl = getListFor(ListKey.KNOWN_SPELLS); if (ksl != null) { aClass.removeListFor(ListKey.KNOWN_SPELLS); for (KnownSpellIdentifier ksi : ksl) { aClass.addToListFor(ListKey.KNOWN_SPELLS, ksi); } } Map<AttackType, Integer> acmap = getMapFor(MapKey.ATTACK_CYCLE); if (acmap != null && !acmap.isEmpty()) { aClass.removeMapFor(MapKey.ATTACK_CYCLE); for (Map.Entry<AttackType, Integer> me : acmap.entrySet()) { aClass.addToMapFor(MapKey.ATTACK_CYCLE, me.getKey(), me .getValue()); } } aClass.levelMap = new TreeMap<>(); for (Map.Entry<Integer, PCClassLevel> me : levelMap.entrySet()) { aClass.levelMap.put(me.getKey(), me.getValue().clone()); } } catch (CloneNotSupportedException exc) { ShowMessageDelegate.showMessageDialog(exc.getMessage(), Constants.APPLICATION_NAME, MessageType.ERROR); } return aClass; } /* * PCCLASSLEVELONLY Since this is really only something that will be done * within a PlayerCharacter (real processing) it is only required in * PCClassLevel. * * As a side note, I'm not sure what I think of accessing the ClassTypes and * using one of those to set the response to this request. Should this be * done when a PCClassLevel is built? Is that possible? How does that * interact with a PlayerCharacter being reimported if those rules change? */ public boolean hasXPPenalty() { for (Type type : getTrueTypeList(false)) { final ClassType aClassType = SettingsHandler.getGame().getClassTypeByName(type.toString()); if ((aClassType != null) && !aClassType.getXPPenalty()) { return false; } } return true; } /** * Get the unarmed Damage for this class at the given level. * * @param aLevel the given level. * @param aPC the PC with the level. * @param adjustForPCSize whether to adjust the result for the PC's size. * @return the unarmed damage string */ public String getUdamForLevel( int aLevel, final PlayerCharacter aPC, boolean adjustForPCSize) { aLevel += (int) aPC.getTotalBonusTo("UDAM", "CLASS." + getKeyName()); return getUDamForEffLevel(aLevel, aPC, adjustForPCSize); } /** * Get the unarmed Damage for this class at the given level. * * @param aLevel the given level. * @param aPC the PC with the level. * @param adjustForPCSize whether to adjust the result for the PC's size. * @return the unarmed damage string */ String getUDamForEffLevel( int aLevel, final PlayerCharacter aPC, boolean adjustForPCSize) { int pcSize = adjustForPCSize ? aPC.sizeInt() : aPC.getDisplay().racialSizeInt(); // // Check "Unarmed Strike", then default to "1d3" // String aDamage; AbstractReferenceContext ref = Globals.getContext().getReferenceContext(); final Equipment eq = ref.silentlyGetConstructedCDOMObject( Equipment.class, "KEY_Unarmed Strike"); if (eq != null) { aDamage = eq.getDamage(aPC); } else { aDamage = "1d3"; } // resize the damage as if it were a weapon if (adjustForPCSize) { int defSize = SizeUtilities.getDefaultSizeAdjustment().get( IntegerKey.SIZEORDER); aDamage = Globals.adjustDamage(aDamage, defSize, pcSize); } // // Check the UDAM list for monk-like damage // List<CDOMObject> classObjects = new ArrayList<>(); //Negative increment to start at highest level until an UDAM is found for (int i = aLevel; i >= 1; i--) { classObjects.add(aPC.getActiveClassLevel(this, i)); } classObjects.add(this); for (CDOMObject cdo : classObjects) { List<String> udam = cdo.getListFor(ListKey.UNARMED_DAMAGE); if (udam != null) { if (udam.size() == 1) { aDamage = udam.get(0); } else { aDamage = udam.get(pcSize); } break; } } return aDamage; } /** * Adds a level of this class to the character. * * TODO: Split the PlayerCharacter code out of PCClass (i.e. the level * property). Then have a joining class assigned to PlayerCharacter that * maps PCClass and number of levels in the class. * * * @param argLevelMax * True if we should only allow extra levels if there are still * levels in this class to take. (i.e. a lot of prestige classes * stop at level 10, so if this is true it would not allow an * 11th level of the class to be added * @param bSilent * True if we are not to show any dialog boxes about errors or * questions. * @param aPC * The character we are adding the level to. * @param ignorePrereqs * True if prereqs for the level should be ignored. Used in * situations such as when the character is being loaded. * @return true or false */ /* * REFACTOR Clearly this is part of the PCClass factory method that produces * PCClassLevels combined with some other work that will need to be done to * extract some of the complicated gunk out of here that goes out and puts * information into PCLevelInfo and PlayerCharacter. */ public boolean addLevel( final boolean argLevelMax, final boolean bSilent, final PlayerCharacter aPC, final boolean ignorePrereqs) { // Check to see if we can add a level of this class to the // current character final int newLevel = aPC.getLevel(this) + 1; boolean levelMax = argLevelMax; aPC.setAllowInteraction(false); aPC.setLevelWithoutConsequence(this, newLevel); if (!ignorePrereqs) { // When loading a character, classes are added before feats, so // this test would always fail on loading if feats are required boolean doReturn = false; if (!qualifies(aPC, this)) { doReturn = true; if (!bSilent) { ShowMessageDelegate.showMessageDialog( "This character does not qualify for level " + newLevel, Constants.APPLICATION_NAME, MessageType.ERROR); } } aPC.setLevelWithoutConsequence(this, newLevel - 1); if (doReturn) { return false; } } aPC.setAllowInteraction(true); if (isMonster()) { levelMax = false; } if (hasMaxLevel() && (newLevel > getSafe(IntegerKey.LEVEL_LIMIT)) && levelMax) { if (!bSilent) { ShowMessageDelegate.showMessageDialog( "This class cannot be raised above level " + Integer.toString(getSafe(IntegerKey.LEVEL_LIMIT)), Constants.APPLICATION_NAME, MessageType.ERROR); } return false; } // Add the level to the current character int total = aPC.getTotalLevels(); // No longer need this since the race now sets a bonus itself and Templates // are not able to reassign their feats. There was nothing else returned in // this number // if (total == 0) { // aPC.setFeats(aPC.getInitialFeats()); // } setLevel(newLevel, aPC); // the level has now been added to the character, // so now assign the attributes of this class level to the // character... PCClassLevel classLevel = aPC.getActiveClassLevel(this, newLevel); // Make sure that if this Class adds a new domain that // we record where that domain came from final int dnum = aPC.getMaxCharacterDomains(this, aPC) - aPC.getDomainCount(); if (dnum > 0 && !aPC.hasDefaultDomainSource()) { aPC.setDefaultDomainSource(new ClassSource(this, newLevel)); } // Don't roll the hit points if the gui is not being used. // This is so GMGen can add classes to a person without pcgen flipping // out if (Globals.getUseGUI()) { final int levels = SettingsHandler.isHPMaxAtFirstClassLevel() ? aPC.totalNonMonsterLevels() : aPC.getTotalLevels(); final boolean isFirst = levels == 1; aPC.rollHP(this, aPC.getLevel(this), isFirst); } if (!aPC.isImporting()) { DomainApplication.addDomainsUpToLevel(this, newLevel, aPC); } int levelUpStats = 0; // Add any bonus feats or stats that will be gained from this level // i.e. a bonus feat every 3 levels if (aPC.getTotalLevels() > total) { boolean processBonusStats = true; total = aPC.getTotalLevels(); if (isMonster()) { // If we have less levels that the races monster levels // then we can not give a stat bonus (i.e. an Ogre has // 4 levels of Giant, so it does not get a stat increase at // 4th level because that is already taken into account in // its racial stat modifiers, but it will get one at 8th LevelCommandFactory lcf = aPC.getRace().get(ObjectKey.MONSTER_CLASS); int monLevels = 0; if (lcf != null) { monLevels = lcf.getLevelCount().resolve(aPC, "").intValue(); } if (total <= monLevels) { processBonusStats = false; } } if (!aPC.isImporting()) { // We do not want to do these // calculations a second time when are // importing a character. The feat // number and the stat point pool are // already saved in the import file. //if (processBonusFeats) { // final double bonusFeats = aPC.getBonusFeatsForNewLevel(this); // if (bonusFeats > 0) { // aPC.adjustFeats(bonusFeats); // } //} if (processBonusStats) { final int bonusStats = Globals.getBonusStatsForLevel(total, aPC); if (bonusStats > 0) { aPC.setPoolAmount(aPC.getPoolAmount() + bonusStats); if (!bSilent && SettingsHandler.getShowStatDialogAtLevelUp()) { levelUpStats = StatApplication.askForStatIncrease(aPC, bonusStats, true); } } } } } int spMod = getSkillPointsForLevel(aPC, classLevel, total); PCLevelInfo pcl; if (aPC.getLevelInfoSize() > 0) { pcl = aPC.getLevelInfo(aPC.getLevelInfoSize() - 1); if (pcl != null) { pcl.setClassLevel(aPC.getLevel(this)); pcl.setSkillPointsGained(aPC, spMod); pcl.setSkillPointsRemaining(pcl.getSkillPointsGained(aPC)); } } Integer currentPool = aPC.getSkillPool(this); int newSkillPool = spMod + (currentPool == null ? 0 : currentPool); aPC.setSkillPool(this, newSkillPool); if (!aPC.isImporting()) { // // Ask for stat increase after skill points have been calculated // if (levelUpStats > 0) { StatApplication.askForStatIncrease(aPC, levelUpStats, false); } if (newLevel == 1) { AddObjectActions.doBaseChecks(this, aPC); CDOMObjectUtilities.addAdds(this, aPC); CDOMObjectUtilities.checkRemovals(this, aPC); } for (TransitionChoice<Kit> kit : classLevel .getSafeListFor(ListKey.KIT_CHOICE)) { kit.act(kit.driveChoice(aPC), classLevel, aPC); } TransitionChoice<Region> region = classLevel .get(ObjectKey.REGION_CHOICE); if (region != null) { region.act(region.driveChoice(aPC), classLevel, aPC); } } // this is a monster class, so don't worry about experience if (isMonster()) { return true; } if (!aPC.isImporting()) { CDOMObjectUtilities.checkRemovals(this, aPC); final int minxp = aPC.minXPForECL(); if (aPC.getXP() < minxp) { aPC.setXP(minxp); } else if (aPC.getXP() >= aPC.minXPForNextECL()) { if (!bSilent) { ShowMessageDelegate.showMessageDialog(SettingsHandler .getGame().getLevelUpMessage(), Constants.APPLICATION_NAME, MessageType.INFORMATION); } } } // // Allow exchange of classes only when assign 1st level // if (containsKey(ObjectKey.EXCHANGE_LEVEL) && (aPC.getLevel(this) == 1) && !aPC.isImporting()) { ExchangeLevelApplication.exchangeLevels(aPC, this); } return true; } public int getSkillPointsForLevel(final PlayerCharacter aPC, PCClassLevel classLevel, int characterLevel) { // Update Skill Points. Modified 20 Nov 2002 by sage_sam // for bug #629643 //final int spMod; int spMod = aPC.recalcSkillPointMod(this, characterLevel); if (classLevel.get(ObjectKey.DONTADD_SKILLPOINTS) != null) { spMod = 0; } return spMod; } /* * DELETEMETHOD I hope this can be deleted, since minus level support will not * work the same way in the new PCClass/PCClassLevel world. If nothing else, it * is massively a REFACTOR item to put this into the PlayerCharacter that is * doing the removal. */ void doMinusLevelMods(final PlayerCharacter aPC, final int oldLevel) { PCClassLevel pcl = aPC.getActiveClassLevel(this, oldLevel); CDOMObjectUtilities.removeAdds(pcl, aPC); CDOMObjectUtilities.restoreRemovals(pcl, aPC); } void subLevel(final PlayerCharacter aPC) { if (aPC != null) { int total = aPC.getTotalLevels(); int oldLevel = aPC.getLevel(this); int spMod = 0; final PCLevelInfo pcl = aPC.getLevelInfoFor(getKeyName(), oldLevel); if (pcl != null) { spMod = pcl.getSkillPointsGained(aPC); } else { Logging .errorPrint("ERROR: could not find class/level info for " + getDisplayName() + "/" + oldLevel); } final int newLevel = oldLevel - 1; if (oldLevel > 0) { PCClassLevel classLevel = aPC.getActiveClassLevel(this, oldLevel-1); aPC.removeHP(classLevel); } // aPC.adjustFeats(-aPC.getBonusFeatsForNewLevel(this)); setLevel(newLevel, aPC); aPC.removeKnownSpellsForClassLevel(this); doMinusLevelMods(aPC, newLevel + 1); DomainApplication.removeDomainsForLevel(this, newLevel + 1, aPC); if (newLevel == 0) { SubClassApplication.setSubClassKey(aPC, this, Constants.NONE); // // Remove all skills associated with this class // for (Skill skill : aPC.getSkillSet()) { SkillRankControl.setZeroRanks(this, aPC, skill); } Integer currentPool = aPC.getSkillPool(this); spMod = currentPool == null ? 0 : currentPool; } if (!isMonster() && (total > aPC.getTotalLevels())) { total = aPC.getTotalLevels(); // Roll back any stat changes that were made as part of the // level final List<PCLevelInfoStat> moddedStats = new ArrayList<>(); if (pcl.getModifiedStats(true) != null) { moddedStats.addAll(pcl.getModifiedStats(true)); } if (pcl.getModifiedStats(false) != null) { moddedStats.addAll(pcl.getModifiedStats(false)); } if (!moddedStats.isEmpty()) { for (PCLevelInfoStat statToRollback : moddedStats) { for (PCStat aStat : aPC.getStatSet()) { if (aStat.equals(statToRollback.getStat())) { aPC.setStat(aStat, aPC.getStat(aStat) - statToRollback.getStatMod()); break; } } } } } aPC.setLevelWithoutConsequence(this, newLevel); if (isMonster() || (total != 0)) { Integer currentPool = aPC.getSkillPool(this); int newSkillPool = (currentPool == null ? 0 : currentPool) - spMod; aPC.setSkillPool(this, newSkillPool); aPC.setDirty(true); } if (aPC.getLevel(this) == 0) { aPC.removeClass(this); } aPC.validateCharacterDomains(); if (!aPC.isImporting()) { final int maxxp = aPC.minXPForNextECL(); if (aPC.getXP() >= maxxp) { aPC.setXP(Math.max(maxxp-1, 0)); } } } else { Logging .errorPrint("No current pc in subLevel()? How did this happen?"); return; } } /* * REFACTOR Some derivative of this method will be in PCClass only as part * of the factory creation of a PCClassLevel... or perhaps in PCClassLevel * so it can steal some information from other PCClassLevels of that * PCClass. Either way, this will be far from its current form in the final * solution. */ /* * CONSIDER Why does this not inherit classSkillChoices? */ public void inheritAttributesFrom(final PCClass otherClass) { Boolean hbss = otherClass.get(ObjectKey.HAS_BONUS_SPELL_STAT); if (hbss != null) { put(ObjectKey.HAS_BONUS_SPELL_STAT, hbss); CDOMSingleRef<PCStat> bss = otherClass.get(ObjectKey.BONUS_SPELL_STAT); if (bss != null) { put(ObjectKey.BONUS_SPELL_STAT, bss); } } Boolean usbs = otherClass.get(ObjectKey.USE_SPELL_SPELL_STAT); if (usbs != null) { put(ObjectKey.USE_SPELL_SPELL_STAT, usbs); } Boolean cwss = otherClass.get(ObjectKey.CASTER_WITHOUT_SPELL_STAT); if (cwss != null) { put(ObjectKey.CASTER_WITHOUT_SPELL_STAT, cwss); } CDOMSingleRef<PCStat> ss = otherClass.get(ObjectKey.SPELL_STAT); if (ss != null) { put(ObjectKey.SPELL_STAT, ss); } TransitionChoice<CDOMListObject<Spell>> slc = otherClass .get(ObjectKey.SPELLLIST_CHOICE); if (slc != null) { put(ObjectKey.SPELLLIST_CHOICE, slc); } List<QualifiedObject<CDOMReference<Equipment>>> e = otherClass .getListFor(ListKey.EQUIPMENT); if (e != null) { addAllToListFor(ListKey.EQUIPMENT, e); } List<WeaponProfProvider> wp = otherClass.getListFor(ListKey.WEAPONPROF); if (wp != null) { addAllToListFor(ListKey.WEAPONPROF, wp); } QualifiedObject<Boolean> otherWP = otherClass .get(ObjectKey.HAS_DEITY_WEAPONPROF); if (otherWP != null) { put(ObjectKey.HAS_DEITY_WEAPONPROF, otherWP); } List<ArmorProfProvider> ap = otherClass .getListFor(ListKey.AUTO_ARMORPROF); if (ap != null) { addAllToListFor(ListKey.AUTO_ARMORPROF, ap); } List<ShieldProfProvider> sp = otherClass .getListFor(ListKey.AUTO_SHIELDPROF); if (sp != null) { addAllToListFor(ListKey.AUTO_SHIELDPROF, sp); } List<BonusObj> bonusList = otherClass.getListFor(ListKey.BONUS); if (bonusList != null) { addAllToListFor(ListKey.BONUS, bonusList); } try { ownBonuses(this); } catch (CloneNotSupportedException ce) { // TODO Auto-generated catch block ce.printStackTrace(); } for (VariableKey vk : otherClass.getVariableKeys()) { put(vk, otherClass.get(vk)); } if (otherClass.containsListFor(ListKey.CSKILL)) { removeListFor(ListKey.CSKILL); addAllToListFor(ListKey.CSKILL, otherClass .getListFor(ListKey.CSKILL)); } if (otherClass.containsListFor(ListKey.LOCALCCSKILL)) { removeListFor(ListKey.LOCALCCSKILL); addAllToListFor(ListKey.LOCALCCSKILL, otherClass .getListFor(ListKey.LOCALCCSKILL)); } removeListFor(ListKey.KIT_CHOICE); addAllToListFor(ListKey.KIT_CHOICE, otherClass .getSafeListFor(ListKey.KIT_CHOICE)); remove(ObjectKey.REGION_CHOICE); if (otherClass.containsKey(ObjectKey.REGION_CHOICE)) { put(ObjectKey.REGION_CHOICE, otherClass .get(ObjectKey.REGION_CHOICE)); } removeListFor(ListKey.SAB); addAllToListFor(ListKey.SAB, otherClass.getSafeListFor(ListKey.SAB)); /* * TODO Does this need to have things from the Class Level objects? * I don't think so based on deferred processing of levels... */ addAllToListFor(ListKey.DAMAGE_REDUCTION, otherClass .getListFor(ListKey.DAMAGE_REDUCTION)); for (CDOMReference<Vision> ref : otherClass .getSafeListMods(Vision.VISIONLIST)) { for (AssociatedPrereqObject apo : otherClass.getListAssociations( Vision.VISIONLIST, ref)) { putToList(Vision.VISIONLIST, ref, apo); } } /* * TODO This is a clone problem, but works for now - thpr 10/3/08 */ if (otherClass instanceof SubClass) { levelMap.clear(); copyLevelsFrom(otherClass); } addAllToListFor(ListKey.NATURAL_WEAPON, otherClass .getListFor(ListKey.NATURAL_WEAPON)); put(ObjectKey.LEVEL_HITDIE, otherClass.get(ObjectKey.LEVEL_HITDIE)); } private SortedMap<Integer, PCClassLevel> levelMap = new TreeMap<>(); public PCClassLevel getOriginalClassLevel(int lvl) { if (!levelMap.containsKey(lvl)) { PCClassLevel classLevel = new PCClassLevel(); classLevel.put(IntegerKey.LEVEL, lvl); classLevel.setName(getDisplayName() + "(" + lvl + ")"); classLevel.put(StringKey.QUALIFIED_KEY, getQualifiedKey()); classLevel.put(ObjectKey.SOURCE_CAMPAIGN, get(ObjectKey.SOURCE_CAMPAIGN)); classLevel.put(StringKey.SOURCE_PAGE, get(StringKey.SOURCE_PAGE)); classLevel.put(StringKey.SOURCE_LONG, get(StringKey.SOURCE_LONG)); classLevel.put(StringKey.SOURCE_SHORT, get(StringKey.SOURCE_SHORT)); classLevel.put(StringKey.SOURCE_WEB, get(StringKey.SOURCE_WEB)); classLevel.put(ObjectKey.SOURCE_DATE, get(ObjectKey.SOURCE_DATE)); classLevel.put(ObjectKey.TOKEN_PARENT, this); levelMap.put(lvl, classLevel); } return levelMap.get(lvl); } public boolean hasOriginalClassLevel(int lvl) { return levelMap.containsKey(lvl); } public Collection<PCClassLevel> getOriginalClassLevelCollection() { return Collections.unmodifiableCollection(levelMap.values()); } public void copyLevelsFrom(PCClass cl) { for (Map.Entry<Integer, PCClassLevel> me : cl.levelMap.entrySet()) { try { PCClassLevel lvl = me.getValue().clone(); lvl.put(StringKey.QUALIFIED_KEY, getQualifiedKey()); lvl.put(ObjectKey.SOURCE_CAMPAIGN, get(ObjectKey.SOURCE_CAMPAIGN)); lvl.put(StringKey.SOURCE_PAGE, get(StringKey.SOURCE_PAGE)); lvl.put(StringKey.SOURCE_LONG, get(StringKey.SOURCE_LONG)); lvl.put(StringKey.SOURCE_SHORT, get(StringKey.SOURCE_SHORT)); lvl.put(StringKey.SOURCE_WEB, get(StringKey.SOURCE_WEB)); lvl.put(ObjectKey.SOURCE_DATE, get(ObjectKey.SOURCE_DATE)); lvl.put(ObjectKey.TOKEN_PARENT, this); lvl.setName(getDisplayName() + "(" + lvl.get(IntegerKey.LEVEL) + ")"); lvl.ownBonuses(this); levelMap.put(me.getKey(), lvl); } catch (CloneNotSupportedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } /** * Clear any data from the class levels. Primarily for use by the Classes * LST editor. */ public void clearClassLevels() { levelMap.clear(); } public String getFullKey() { return getKeyName(); } @Override public void ownBonuses(Object owner) throws CloneNotSupportedException { super.ownBonuses(owner); for (PCClassLevel pcl : this.getOriginalClassLevelCollection()) { pcl.ownBonuses(owner); } } @Override public boolean qualifies(PlayerCharacter aPC, Object owner) { if (Globals.checkRule(RuleConstants.CLASSPRE)) { return true; } return super.qualifies(aPC, owner); } /* (non-Javadoc) * @see pcgen.core.facade.ClassFacade#getBaseStat() */ @Override public String getBaseStat() { return getSpellBaseStat(); } /* (non-Javadoc) * @see pcgen.core.facade.ClassFacade#getHD() */ @Override public String getHD() { HitDie hd = getSafe(ObjectKey.LEVEL_HITDIE); return String.valueOf(hd.getDie()); } /* (non-Javadoc) * @see pcgen.core.facade.ClassFacade#getTypes() */ @Override public String[] getTypes() { String type = getType(); return type.split("\\."); } public String getClassType() { FactKey<String> fk = FactKey.valueOf("ClassType"); return getResolved(fk); } }