/* * SpellSupportForPCClass * Copyright 2009 (c) Tom Parker <thpr@users.sourceforge.net> * derived from 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 */ package pcgen.core; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import pcgen.base.formula.Formula; import pcgen.cdom.base.Constants; import pcgen.cdom.content.BonusSpellInfo; import pcgen.cdom.content.KnownSpellIdentifier; import pcgen.cdom.enumeration.AssociationKey; import pcgen.cdom.enumeration.IntegerKey; import pcgen.cdom.enumeration.ListKey; import pcgen.cdom.enumeration.ObjectKey; import pcgen.cdom.inst.PCClassLevel; import pcgen.core.analysis.SpellCountCalc; import pcgen.core.character.CharacterSpell; import pcgen.core.spell.Spell; public class SpellSupportForPCClass { /* * ALLCLASSLEVELS castForLevelMap is part of PCClassLevel - or nothing at * all since this seems to be a form of cache? - DELETEVARIABLE */ private HashMap<Integer, Integer> castForLevelMap = null; private SpellProgressionCache spellCache = null; private boolean spellCacheValid = false; private final PCClass source; public SpellSupportForPCClass(PCClass cis) { source = cis; } /** * Get the highest level of spell that this class can cast. * * @return the highest level of spells that this class can cast, or -1 if * this class can not cast spells */ /* * PCCLASSLEVELONLY This calculation is dependent upon the class level and * is therefore appropriate only for PCClassLevel */ public int getMaxCastLevel() { int currHighest = -1; if (castForLevelMap != null) { for (int key : castForLevelMap.keySet()) { final Integer value = castForLevelMap.get(key); if (value != null) { if (value > 0 && key > currHighest) { currHighest = key; } } } } return currHighest; } /** * Get the highest level of spell that this class can cast. * * @param aPC The character to build the casting information for. * @return the highest level of spells that this class can cast, or -1 if * this class can not cast spells */ public int getMaxCastLevel(PlayerCharacter aPC) { if (castForLevelMap == null) { calcCastPerDayMapForLevel(aPC); } return getMaxCastLevel(); } public List<Formula> getCastListForLevel(int aLevel) { if (!updateSpellCache(false)) { return null; } return spellCache.getCastForLevel(aLevel); } public boolean hasCastList() { return updateSpellCache(false) && spellCache.hasCastProgression(); } public int getHighestLevelSpell() { if (!updateSpellCache(false)) { return -1; } return Math.max(spellCache.getHighestCastSpellLevel(), spellCache .getHighestKnownSpellLevel()); } /** * Identify if the character can cast spells for this class. This will take * into account the class casting progression as well as the character's * bonuses. * @param aPC The character to be checked. * @return true if the character can cast spells for this class, false if not. */ public boolean canCastSpells(PlayerCharacter aPC) { if (!updateSpellCache(false) || !spellCache.hasCastProgression()) { return false; } for (int i = 0; i < 100; i++) { final int numSpellsCastable = getCastForLevel(i, aPC); if (numSpellsCastable > 0) { return true; } } // No casting ability found return false; } public int getKnownForLevel(int spellLevel, PlayerCharacter aPC) { int total = 0; int stat = 0; final String classKeyName = "CLASS." + source.getKeyName(); final String levelSpellLevel = ";LEVEL." + spellLevel; final String allSpellLevel = ";LEVEL.All"; int pcLevel = aPC.getLevel(source); pcLevel += (int) aPC.getTotalBonusTo("PCLEVEL", source.getKeyName()); pcLevel += (int) aPC.getTotalBonusTo("PCLEVEL", "TYPE." + source.getSpellType()); /* * CONSIDER Why is known testing getNumFromCastList??? - thpr 11/8/06 */ if (updateSpellCache(false) && spellCache.hasCastProgression() && (getNumFromCastList(pcLevel, spellLevel, aPC) < 0)) { // Don't know any spells of this level // however, character might have a bonus spells e.g. from certain // feats return (int) aPC.getTotalBonusTo("SPELLKNOWN", classKeyName + levelSpellLevel); } total += (int) aPC.getTotalBonusTo("SPELLKNOWN", classKeyName + levelSpellLevel); total += (int) aPC.getTotalBonusTo("SPELLKNOWN", "TYPE." + source.getSpellType() + levelSpellLevel); total += (int) aPC.getTotalBonusTo("SPELLKNOWN", "CLASS.Any" + levelSpellLevel); total += (int) aPC.getTotalBonusTo("SPELLKNOWN", classKeyName + allSpellLevel); total += (int) aPC.getTotalBonusTo("SPELLKNOWN", "TYPE." + source.getSpellType() + allSpellLevel); total += (int) aPC.getTotalBonusTo("SPELLKNOWN", "CLASS.Any" + allSpellLevel); PCStat aStat = source.baseSpellStat(); String statString = Constants.NONE; if (aStat != null) { stat = aPC.getTotalStatFor(aStat); statString = aStat.getKeyName(); } final int bonusStat = (int) aPC.getTotalBonusTo("STAT", "KNOWN." + statString) + (int) aPC.getTotalBonusTo("STAT", "BASESPELLKNOWNSTAT") + (int) aPC.getTotalBonusTo("STAT", "BASESPELLKNOWNSTAT;CLASS=" + source.getKeyName()); if (!source.getSafe(ObjectKey.USE_SPELL_SPELL_STAT) && !source.getSafe(ObjectKey.CASTER_WITHOUT_SPELL_STAT)) { final int maxSpellLevel = aPC.getVariableValue( "MAXLEVELSTAT=" + statString, "").intValue(); if ((maxSpellLevel + bonusStat) < spellLevel) { return total; } } stat += bonusStat; int mult = (int) aPC.getTotalBonusTo("SPELLKNOWNMULT", classKeyName + levelSpellLevel); mult += (int) aPC.getTotalBonusTo("SPELLKNOWNMULT", "TYPE." + source.getSpellType() + levelSpellLevel); if (mult < 1) { mult = 1; } if (!updateSpellCache(false)) { return total; } if (spellCache.hasKnownProgression()) { List<Formula> knownList = spellCache.getKnownForLevel(pcLevel); if (spellLevel >= 0 && knownList != null && spellLevel < knownList.size()) { total += mult * knownList.get(spellLevel).resolve(aPC, "").intValue(); // add Stat based bonus BonusSpellInfo bsi = Globals.getContext().getReferenceContext() .silentlyGetConstructedCDOMObject(BonusSpellInfo.class, String.valueOf(spellLevel)); if (Globals.checkRule(RuleConstants.BONUSSPELLKNOWN) && (bsi != null) && bsi.isValid()) { int base = bsi.getStatScore(); if (stat >= base) { int range = bsi.getStatRange(); total += Math.max(0, (stat - base + range) / range); } } } } // if we have known spells (0==no known spells recorded) // or a psi specialty. if (total > 0 && spellLevel > 0) { // make sure any slots due from specialties total += source.getSafe(IntegerKey.KNOWN_SPELLS_FROM_SPECIALTY); // (including domains) are added Integer assoc = aPC.getDomainSpellCount(source); if (assoc != null) { total += assoc; } } // Add in any from SPELLKNOWN total += aPC.getKnownSpellCountForLevel(source .get(ObjectKey.CLASS_SPELLLIST), spellLevel); return total; } public int getMinLevelForSpellLevel(int spellLevel, boolean allowBonus) { if (!updateSpellCache(false)) { return -1; } return spellCache.getMinLevelForSpellLevel(spellLevel, allowBonus); } public int getMaxSpellLevelForClassLevel(int classLevel) { if (!updateSpellCache(false)) { return -1; } return spellCache.getMaxSpellLevelForClassLevel(classLevel); } public boolean hasKnownList() { return updateSpellCache(false) && spellCache.hasKnownProgression(); } public int getSpecialtyKnownForLevel(int spellLevel, PlayerCharacter aPC) { int total; total = (int) aPC.getTotalBonusTo("SPECIALTYSPELLKNOWN", "CLASS." + source.getKeyName() + ";LEVEL." + spellLevel); total += (int) aPC.getTotalBonusTo("SPECIALTYSPELLKNOWN", "TYPE." + source.getSpellType() + ";LEVEL." + spellLevel); int pcLevel = aPC.getLevel(source); pcLevel += (int) aPC.getTotalBonusTo("PCLEVEL", source.getKeyName()); pcLevel += (int) aPC.getTotalBonusTo("PCLEVEL", "TYPE." + source.getSpellType()); PCStat aStat = source.baseSpellStat(); if (aStat != null) { final int maxSpellLevel = aPC.getVariableValue( "MAXLEVELSTAT=" + aStat.getKeyName(), "").intValue(); if (spellLevel > maxSpellLevel) { return total; } } if (updateSpellCache(false)) { List<Formula> specKnown = spellCache .getSpecialtyKnownForLevel(pcLevel); if (specKnown != null && specKnown.size() > spellLevel) { total += specKnown.get(spellLevel).resolve(aPC, "").intValue(); } } // make sure any slots due from specialties total += source.getSafe(IntegerKey.KNOWN_SPELLS_FROM_SPECIALTY); // (including domains) are added Integer assoc = aPC.getDomainSpellCount(source); if (assoc != null) { total += assoc; } return total; } public boolean updateSpellCache(boolean force) { if (force || !spellCacheValid) { SpellProgressionCache cache = new SpellProgressionCache(); for (PCClassLevel cl : source.getOriginalClassLevelCollection()) { Integer lvl = cl.get(IntegerKey.LEVEL); List<Formula> cast = cl.getListFor(ListKey.CAST); if (cast != null) { cache.setCast(lvl, cast); } List<Formula> known = cl.getListFor(ListKey.KNOWN); if (known != null) { cache.setKnown(lvl, known); } List<Formula> spec = cl.getListFor(ListKey.SPECIALTYKNOWN); if (spec != null) { cache.setSpecialtyKnown(lvl, spec); } } if (!cache.isEmpty()) { spellCache = cache; } spellCacheValid = true; } return spellCache != null; } /** * Build a caster level map for this class. The map will be of the form * <Integer,Integer> where the key is the spell level and the value is the * number of times per day that spell level can be cast by the character * * @param aPC */ /* * PCCLASSLEVELONLY This calculation is dependent upon the class level and * is therefore appropriate only for PCClassLevel */ void calcCastPerDayMapForLevel(final PlayerCharacter aPC) { // // TODO: Shouldn't we be using Globals.getLevelInfo().size() instead of // 100? // Byngl -- November 25, 2002 // if (castForLevelMap == null) { castForLevelMap = new HashMap<>(100); } for (int i = 0; i < 100; i++) { final int s = getCastForLevel(i, aPC); castForLevelMap.put(i, s); } } public boolean isAutoKnownSpell(Spell aSpell, int spellLevel, boolean useMap, PlayerCharacter aPC) { List<KnownSpellIdentifier> knownSpellsList = source.getListFor(ListKey.KNOWN_SPELLS); if (knownSpellsList == null) { return false; } if (useMap) { final Integer val = castForLevelMap.get(spellLevel); if ((val == null) || val == 0 || (aSpell == null)) { return false; } } else if ((getCastForLevel(spellLevel, aPC) == 0) || (aSpell == null)) { return false; } if (SpellCountCalc.isProhibited(aSpell, source, aPC) && !SpellCountCalc.isSpecialtySpell(aPC, source, aSpell)) { return false; } // iterate through the KNOWNSPELLS: tag for (KnownSpellIdentifier filter : knownSpellsList) { if (filter.matchesFilter(aSpell, spellLevel)) { return true; } } return false; } public int getNumFromCastList(int iCasterLevel, int iSpellLevel, PlayerCharacter aPC) { if (iCasterLevel == 0) { // can't cast spells! return -1; } List<Formula> castListForLevel = getCastListForLevel(iCasterLevel); if (castListForLevel == null || iSpellLevel >= castListForLevel.size()) { return -1; } return castListForLevel.get(iSpellLevel).resolve(aPC, "").intValue(); } public int getCastForLevel(int spellLevel, PlayerCharacter aPC) { return getCastForLevel(spellLevel, Globals.getDefaultSpellBook(), true, true, aPC); } public int getCastForLevel(int spellLevel, String bookName, boolean includeAdj, boolean limitByStat, PlayerCharacter aPC) { int pcLevel = aPC.getLevel(source); int total = 0; int stat = 0; final String classKeyName = "CLASS." + source.getKeyName(); final String levelSpellLevel = ";LEVEL." + spellLevel; final String allSpellLevel = ";LEVEL.All"; pcLevel += (int) aPC.getTotalBonusTo("PCLEVEL", source.getKeyName()); pcLevel += (int) aPC.getTotalBonusTo("PCLEVEL", "TYPE." + source.getSpellType()); if (getNumFromCastList(pcLevel, spellLevel, aPC) < 0) { // can't cast spells of this level // however, character might have a bonus spell slot e.g. from // certain feats return (int) aPC.getTotalBonusTo("SPELLCAST", classKeyName + levelSpellLevel); } total += (int) aPC.getTotalBonusTo("SPELLCAST", classKeyName + levelSpellLevel); total += (int) aPC.getTotalBonusTo("SPELLCAST", "TYPE." + source.getSpellType() + levelSpellLevel); total += (int) aPC.getTotalBonusTo("SPELLCAST", "CLASS.Any" + levelSpellLevel); total += (int) aPC.getTotalBonusTo("SPELLCAST", classKeyName + allSpellLevel); total += (int) aPC.getTotalBonusTo("SPELLCAST", "TYPE." + source.getSpellType() + allSpellLevel); total += (int) aPC.getTotalBonusTo("SPELLCAST", "CLASS.Any" + allSpellLevel); PCStat aStat = source.bonusSpellStat(); String statString = Constants.NONE; if (aStat != null) { stat = aPC.getTotalStatFor(aStat); statString = aStat.getKeyName(); } final int bonusStat = (int) aPC.getTotalBonusTo("STAT", "CAST." + statString) + (int) aPC.getTotalBonusTo("STAT", "BASESPELLSTAT") + (int) aPC.getTotalBonusTo("STAT", "BASESPELLSTAT;CLASS=" + source.getKeyName()); if (limitByStat) { PCStat ss = source.baseSpellStat(); if (ss != null) { final int maxSpellLevel = aPC.getVariableValue( "MAXLEVELSTAT=" + ss.getKeyName(), "").intValue(); if ((maxSpellLevel + bonusStat) < spellLevel) { return total; } } } stat += bonusStat; // Now we decide whether to adjust the number of slots down // the road by adding specialty slots. // Reworked to consider the fact that a lower-level // specialty spell can go into this level of specialty slot // int adj = 0; if (includeAdj && !bookName.equals(Globals.getDefaultSpellBook()) && (aPC.hasAssocs(source, AssociationKey.SPECIALTY) || aPC .hasDomains())) { // We need to do this for EVERY spell level up to the // one really under consideration, because if there // are any specialty spells available BELOW this level, // we might wind up using THIS level's slots for them. for (int ix = 0; ix <= spellLevel; ++ix) { Collection<CharacterSpell> aList = aPC.getCharacterSpells( source, ix); Collection<Spell> bList = new ArrayList<>(); if (!aList.isEmpty()) { // Assume no null check on castInfo requried, because // getNumFromCastList above would have returned -1 if ((ix > 0) && "DIVINE".equalsIgnoreCase(source.getSpellType())) { for (Domain d : aPC.getDomainSet()) { if (source.getKeyName().equals( aPC.getDomainSource(d).getPcclass() .getKeyName())) { bList = aPC.getSpellsIn(d.get(ObjectKey.DOMAIN_SPELLLIST), ix); } } } for (CharacterSpell cs : aList) { int x = -1; if (!bList.isEmpty()) { if (bList.contains(cs.getSpell())) { x = 0; } } else { x = cs.getInfoIndexFor(aPC, Constants.EMPTY_STRING, ix, 1); } if (x > -1) { PCClass target = source; String subClassKey = aPC.getSubClassName(source); if (subClassKey != null && (!subClassKey.isEmpty()) && !subClassKey.equals(Constants.NONE)) { target = source.getSubClassKeyed(subClassKey); } adj = aPC.getSpellSupport(target).getSpecialtyKnownForLevel(spellLevel, aPC); break; } } } // end of what to do if aList is not empty if (adj > 0) { break; } } // end of looping up to this level looking for specialty spells that // can be cast } // end of deciding whether there are specialty slots to distribute int mult = (int) aPC.getTotalBonusTo("SPELLCASTMULT", classKeyName + levelSpellLevel); mult += (int) aPC.getTotalBonusTo("SPELLCASTMULT", "TYPE." + source.getSpellType() + levelSpellLevel); if (mult < 1) { mult = 1; } final int t = getNumFromCastList(pcLevel, spellLevel, aPC); total += ((t * mult) + adj); BonusSpellInfo bsi = Globals.getContext().getReferenceContext() .silentlyGetConstructedCDOMObject(BonusSpellInfo.class, String .valueOf(spellLevel)); if ((bsi != null) && bsi.isValid()) { int base = bsi.getStatScore(); if (stat >= base) { int range = bsi.getStatRange(); total += Math.max(0, (stat - base + range) / range); } } return total; } public int getHighestLevelSpell(PlayerCharacter pc) { final String classKeyName = "CLASS." + source.getKeyName(); int mapHigh = getHighestLevelSpell(); int high = mapHigh; for (int i = mapHigh; i < mapHigh + 30; i++) { final String levelSpellLevel = ";LEVEL." + i; if (pc.getTotalBonusTo("SPELLCAST", classKeyName + levelSpellLevel) > 0) { high = i; } else if (pc.getTotalBonusTo("SPELLKNOWN", classKeyName + levelSpellLevel) > 0) { high = i; } } return high; } public String getBonusCastForLevelString(int spellLevel, String bookName, PlayerCharacter aPC) { if (getCastForLevel(spellLevel, bookName, true, true, aPC) > 0) { // if this class has a specialty, return +1 if (aPC.hasAssocs(source, AssociationKey.SPECIALTY)) { PCClass target = source; String subClassKey = aPC.getSubClassName(source); if (subClassKey != null && (!subClassKey.isEmpty()) && !subClassKey.equals(Constants.NONE)) { target = source.getSubClassKeyed(subClassKey); } return "+" + aPC.getSpellSupport(target).getSpecialtyKnownForLevel(spellLevel, aPC); } if (!aPC.hasDomains()) { return ""; } // if the spelllevel is >0 and this class has a characterdomain // associated with it, return +1 if ((spellLevel > 0) && "DIVINE".equalsIgnoreCase(source.getSpellType())) { for (Domain d : aPC.getDomainSet()) { if (source.getKeyName().equals( aPC.getDomainSource(d).getPcclass().getKeyName())) { return "+1"; } } } } return ""; } public boolean hasKnownSpells(PlayerCharacter aPC) { for (int i = 0; i <= getHighestLevelSpell(); i++) { if (getKnownForLevel(i, aPC) > 0) { return true; } } return false; } }