pcgen.core.PCClass.java Source code

Java tutorial

Introduction

Here is the source code for pcgen.core.PCClass.java

Source

/*
 * 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
 */
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.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.facade.core.ClassFacade;
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}.
 */
public class PCClass extends PObject implements ClassFacade, Cloneable {

    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;

    /**
     * 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 '" //$NON-NLS-1$//$NON-NLS-2$
                                    + getClass().getName()
                                    + ".getBonusTo(String,String,int)' rather than in the persistence layer?"); //$NON-NLS-1$
                            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 == 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);
    }

    @Override
    public String getBaseStat() {
        return getSpellBaseStat();
    }

    @Override
    public String getHD() {
        HitDie hd = getSafe(ObjectKey.LEVEL_HITDIE);
        return String.valueOf(hd.getDie());
    }

    @Override
    public String[] getTypes() {
        String type = getType();
        return type.split("\\.");
    }

    public String getClassType() {
        FactKey<String> fk = FactKey.valueOf("ClassType");
        return getResolved(fk);
    }

}