/* * * 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.awt.Rectangle; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeSet; import pcgen.base.formula.Formula; import pcgen.base.formula.base.ScopeInstance; import pcgen.base.formula.base.VarScoped; import pcgen.base.solver.AggressiveSolverManager; import pcgen.base.solver.IndividualSetup; import pcgen.base.solver.SolverFactory; import pcgen.base.solver.SplitFormulaSetup; import pcgen.base.util.HashMapToList; import pcgen.base.util.IdentityList; import pcgen.cdom.base.AssociatedPrereqObject; import pcgen.cdom.base.BonusContainer; import pcgen.cdom.base.CDOMList; import pcgen.cdom.base.CDOMListObject; import pcgen.cdom.base.CDOMObject; import pcgen.cdom.base.CDOMObjectUtilities; import pcgen.cdom.base.CDOMReference; import pcgen.cdom.base.Category; import pcgen.cdom.base.ChooseDriver; import pcgen.cdom.base.ChooseInformation; import pcgen.cdom.base.Constants; import pcgen.cdom.base.TransitionChoice; import pcgen.cdom.content.AbilitySelection; import pcgen.cdom.content.CNAbility; import pcgen.cdom.content.CNAbilityFactory; import pcgen.cdom.content.HitDie; import pcgen.cdom.content.LevelCommandFactory; import pcgen.cdom.content.Processor; import pcgen.cdom.content.RollMethod; import pcgen.cdom.content.VarModifier; import pcgen.cdom.enumeration.AssociationKey; import pcgen.cdom.enumeration.AssociationListKey; import pcgen.cdom.enumeration.BiographyField; import pcgen.cdom.enumeration.CharID; import pcgen.cdom.enumeration.EquipmentLocation; import pcgen.cdom.enumeration.FactKey; import pcgen.cdom.enumeration.FormulaKey; import pcgen.cdom.enumeration.Gender; import pcgen.cdom.enumeration.Handed; import pcgen.cdom.enumeration.IntegerKey; import pcgen.cdom.enumeration.ListKey; import pcgen.cdom.enumeration.MapKey; import pcgen.cdom.enumeration.Nature; import pcgen.cdom.enumeration.NumericPCAttribute; import pcgen.cdom.enumeration.ObjectKey; import pcgen.cdom.enumeration.PCStringKey; import pcgen.cdom.enumeration.Region; import pcgen.cdom.enumeration.SkillCost; import pcgen.cdom.enumeration.SkillFilter; import pcgen.cdom.enumeration.SkillsOutputOrder; import pcgen.cdom.enumeration.StringKey; import pcgen.cdom.enumeration.StringPCAttribute; import pcgen.cdom.enumeration.Type; import pcgen.cdom.enumeration.VariableKey; import pcgen.cdom.facet.ActiveSpellsFacet; import pcgen.cdom.facet.AddedBonusFacet; import pcgen.cdom.facet.AddedTemplateFacet; import pcgen.cdom.facet.AppliedBonusFacet; import pcgen.cdom.facet.AutoEquipmentFacet; import pcgen.cdom.facet.AutoLanguageGrantedFacet; import pcgen.cdom.facet.AvailableSpellFacet; import pcgen.cdom.facet.BonusChangeFacet; import pcgen.cdom.facet.BonusSkillRankChangeFacet; import pcgen.cdom.facet.CheckBonusFacet; import pcgen.cdom.facet.ClassSpellListFacet; import pcgen.cdom.facet.ConditionalAbilityFacet; import pcgen.cdom.facet.ConditionallyGrantedAbilityFacet; import pcgen.cdom.facet.ConditionallyGrantedAvailableSpellFacet; import pcgen.cdom.facet.ConditionallyGrantedKnownSpellFacet; import pcgen.cdom.facet.DirectAbilityFacet; import pcgen.cdom.facet.DomainSpellCountFacet; import pcgen.cdom.facet.EquipSetFacet; import pcgen.cdom.facet.EquipmentFacet; import pcgen.cdom.facet.EquippedEquipmentFacet; import pcgen.cdom.facet.FacetLibrary; import pcgen.cdom.facet.FormulaSetupFacet; import pcgen.cdom.facet.GlobalModifierFacet; import pcgen.cdom.facet.GrantedAbilityFacet; import pcgen.cdom.facet.HitPointFacet; import pcgen.cdom.facet.KitFacet; import pcgen.cdom.facet.KnownSpellFacet; import pcgen.cdom.facet.LevelInfoFacet; import pcgen.cdom.facet.MasterFacet; import pcgen.cdom.facet.NoteItemFacet; import pcgen.cdom.facet.PlayerCharacterTrackingFacet; import pcgen.cdom.facet.PrimaryWeaponFacet; import pcgen.cdom.facet.SaveableBonusFacet; import pcgen.cdom.facet.SavedAbilitiesFacet; import pcgen.cdom.facet.ScopeFacet; import pcgen.cdom.facet.SecondaryWeaponFacet; import pcgen.cdom.facet.SkillCostFacet; import pcgen.cdom.facet.SkillOutputOrderFacet; import pcgen.cdom.facet.SkillPoolFacet; import pcgen.cdom.facet.SkillRankFacet; import pcgen.cdom.facet.SolverFactoryFacet; import pcgen.cdom.facet.SolverManagerFacet; import pcgen.cdom.facet.SourcedEquipmentFacet; import pcgen.cdom.facet.SpellBookFacet; import pcgen.cdom.facet.SpellListFacet; import pcgen.cdom.facet.SpellProhibitorFacet; import pcgen.cdom.facet.SpellSupportFacet; import pcgen.cdom.facet.StartingLanguageFacet; import pcgen.cdom.facet.StatBonusFacet; import pcgen.cdom.facet.StatCalcFacet; import pcgen.cdom.facet.StatValueFacet; import pcgen.cdom.facet.SubClassFacet; import pcgen.cdom.facet.SubstitutionClassFacet; import pcgen.cdom.facet.TargetTrackingFacet; import pcgen.cdom.facet.TemplateFeatFacet; import pcgen.cdom.facet.UserEquipmentFacet; import pcgen.cdom.facet.VariableStoreFacet; import pcgen.cdom.facet.XPTableFacet; import pcgen.cdom.facet.analysis.AgeSetFacet; import pcgen.cdom.facet.analysis.ChangeProfFacet; import pcgen.cdom.facet.analysis.CharacterSpellResistanceFacet; import pcgen.cdom.facet.analysis.FavoredClassFacet; import pcgen.cdom.facet.analysis.FollowerLimitFacet; import pcgen.cdom.facet.analysis.LegalDeityFacet; import pcgen.cdom.facet.analysis.LevelFacet; import pcgen.cdom.facet.analysis.LevelTableFacet; import pcgen.cdom.facet.analysis.LoadFacet; import pcgen.cdom.facet.analysis.MovementResultFacet; import pcgen.cdom.facet.analysis.NonAbilityFacet; import pcgen.cdom.facet.analysis.NonStatStatFacet; import pcgen.cdom.facet.analysis.NonStatToStatFacet; import pcgen.cdom.facet.analysis.QualifyFacet; import pcgen.cdom.facet.analysis.ResultFacet; import pcgen.cdom.facet.analysis.SpecialAbilityFacet; import pcgen.cdom.facet.analysis.StatLockFacet; import pcgen.cdom.facet.analysis.UnlockedStatFacet; import pcgen.cdom.facet.analysis.VariableFacet; import pcgen.cdom.facet.base.AbstractStorageFacet; import pcgen.cdom.facet.fact.AgeFacet; import pcgen.cdom.facet.fact.AllowDebtFacet; import pcgen.cdom.facet.fact.CharacterTypeFacet; import pcgen.cdom.facet.fact.ChronicleEntryFacet; import pcgen.cdom.facet.fact.FactFacet; import pcgen.cdom.facet.fact.FollowerFacet; import pcgen.cdom.facet.fact.GenderFacet; import pcgen.cdom.facet.fact.GoldFacet; import pcgen.cdom.facet.fact.HandedFacet; import pcgen.cdom.facet.fact.HeightFacet; import pcgen.cdom.facet.fact.IgnoreCostFacet; import pcgen.cdom.facet.fact.PortraitThumbnailRectFacet; import pcgen.cdom.facet.fact.PreviewSheetFacet; import pcgen.cdom.facet.fact.RegionFacet; import pcgen.cdom.facet.fact.SkillFilterFacet; import pcgen.cdom.facet.fact.SuppressBioFieldFacet; import pcgen.cdom.facet.fact.WeightFacet; import pcgen.cdom.facet.fact.XPFacet; import pcgen.cdom.facet.input.AddLanguageFacet; import pcgen.cdom.facet.input.AutoEquipmentListFacet; import pcgen.cdom.facet.input.AutoLanguageListFacet; import pcgen.cdom.facet.input.AutoListArmorProfFacet; import pcgen.cdom.facet.input.AutoListShieldProfFacet; import pcgen.cdom.facet.input.AutoListWeaponProfFacet; import pcgen.cdom.facet.input.BonusWeaponProfFacet; import pcgen.cdom.facet.input.CampaignFacet; import pcgen.cdom.facet.input.DomainInputFacet; import pcgen.cdom.facet.input.FreeLanguageFacet; import pcgen.cdom.facet.input.GlobalAddedSkillCostFacet; import pcgen.cdom.facet.input.LocalAddedSkillCostFacet; import pcgen.cdom.facet.input.MonsterCSkillFacet; import pcgen.cdom.facet.input.ProhibitedSchoolFacet; import pcgen.cdom.facet.input.RaceInputFacet; import pcgen.cdom.facet.input.TemplateInputFacet; import pcgen.cdom.facet.input.UserSpecialAbilityFacet; import pcgen.cdom.facet.model.AlignmentFacet; import pcgen.cdom.facet.model.ArmorProfProviderFacet; import pcgen.cdom.facet.model.BioSetFacet; import pcgen.cdom.facet.model.CheckFacet; import pcgen.cdom.facet.model.ClassFacet; import pcgen.cdom.facet.model.CompanionModFacet; import pcgen.cdom.facet.model.DeityFacet; import pcgen.cdom.facet.model.DomainFacet; import pcgen.cdom.facet.model.ExpandedCampaignFacet; import pcgen.cdom.facet.model.LanguageFacet; import pcgen.cdom.facet.model.RaceFacet; import pcgen.cdom.facet.model.ShieldProfProviderFacet; import pcgen.cdom.facet.model.SizeFacet; import pcgen.cdom.facet.model.SkillFacet; import pcgen.cdom.facet.model.StatFacet; import pcgen.cdom.facet.model.TemplateFacet; import pcgen.cdom.facet.model.WeaponProfModelFacet; import pcgen.cdom.formula.MonitorableVariableStore; import pcgen.cdom.helper.CNAbilitySelection; import pcgen.cdom.helper.ClassSource; import pcgen.cdom.helper.ProfProvider; import pcgen.cdom.helper.SAProcessor; import pcgen.cdom.helper.SAtoStringProcessor; import pcgen.cdom.helper.SpringHelper; import pcgen.cdom.identifier.SpellSchool; import pcgen.cdom.inst.CodeControl; import pcgen.cdom.inst.EquipmentHead; import pcgen.cdom.inst.GlobalModifiers; import pcgen.cdom.inst.ObjectCache; import pcgen.cdom.inst.PCClassLevel; import pcgen.cdom.list.AbilityList; import pcgen.cdom.list.ClassSpellList; import pcgen.cdom.list.CompanionList; import pcgen.cdom.list.DomainSpellList; import pcgen.cdom.reference.CDOMGroupRef; import pcgen.cdom.reference.CDOMSingleRef; import pcgen.cdom.util.CControl; import pcgen.cdom.util.ControlUtilities; import pcgen.core.BonusManager.TempBonusInfo; import pcgen.core.analysis.BonusCalc; import pcgen.core.analysis.ChooseActivation; import pcgen.core.analysis.DomainApplication; import pcgen.core.analysis.SkillModifier; import pcgen.core.analysis.SkillRankControl; import pcgen.core.analysis.SpellCountCalc; import pcgen.core.analysis.SpellLevel; import pcgen.core.analysis.StatAnalysis; import pcgen.core.bonus.BonusObj; import pcgen.core.bonus.BonusPair; import pcgen.core.bonus.BonusUtilities; import pcgen.core.character.CharacterSpell; import pcgen.core.character.CompanionMod; import pcgen.core.character.EquipSet; import pcgen.core.character.EquipSlot; import pcgen.core.character.Follower; import pcgen.core.character.SpellBook; import pcgen.core.character.SpellInfo; import pcgen.core.chooser.ChoiceManagerList; import pcgen.core.chooser.ChooserUtilities; import pcgen.core.display.CharacterDisplay; import pcgen.core.display.SkillDisplay; import pcgen.core.pclevelinfo.PCLevelInfo; import pcgen.core.spell.Spell; import pcgen.core.utils.CoreUtility; import pcgen.core.utils.MessageType; import pcgen.core.utils.ShowMessageDelegate; import pcgen.io.PCGFile; import pcgen.io.exporttoken.EqToken; import pcgen.rules.context.AbstractReferenceContext; import pcgen.rules.context.LoadContext; import pcgen.system.PCGenSettings; import pcgen.util.Delta; import pcgen.util.Logging; import pcgen.util.enumeration.AttackType; import pcgen.util.enumeration.Load; /** * {@code PlayerCharacter}. * * @author Bryan McRoberts <merton_monk@users.sourceforge.net> */ public class PlayerCharacter implements Cloneable, VariableContainer { // Constants for use in getBonus private static String lastVariable = null; // This marker is static so that the spells allocated to it can also be found in the cloned character. private static ObjectCache grantedSpellCache = new ObjectCache(); private final CharID id; private final SAtoStringProcessor SA_TO_STRING_PROC; private final SAProcessor SA_PROC; private final CharacterDisplay display; /* * Note "pure" here means no getDirty call, and absolutely no other stuff in * the method. Also any method is not used elsewhere in PlayerCharacter */ //The following facets are write-only isolated (dirty in a set is allowed) private AllowDebtFacet allowDebtFacet = FacetLibrary.getFacet(AllowDebtFacet.class); private ChronicleEntryFacet chronicleEntryFacet = FacetLibrary.getFacet(ChronicleEntryFacet.class); private IgnoreCostFacet ignoreCostFacet = FacetLibrary.getFacet(IgnoreCostFacet.class); private GenderFacet genderFacet = FacetLibrary.getFacet(GenderFacet.class); private HandedFacet handedFacet = FacetLibrary.getFacet(HandedFacet.class); private HeightFacet heightFacet = FacetLibrary.getFacet(HeightFacet.class); private WeightFacet weightFacet = FacetLibrary.getFacet(WeightFacet.class); private AddLanguageFacet addLangFacet = FacetLibrary.getFacet(AddLanguageFacet.class); private AutoLanguageListFacet autoLangListFacet = FacetLibrary.getFacet(AutoLanguageListFacet.class); private FreeLanguageFacet freeLangFacet = FacetLibrary.getFacet(FreeLanguageFacet.class); private CharacterTypeFacet characterTypeFacet = FacetLibrary.getFacet(CharacterTypeFacet.class); private SuppressBioFieldFacet suppressBioFieldFacet = FacetLibrary.getFacet(SuppressBioFieldFacet.class); private AutoListArmorProfFacet armorProfListFacet = FacetLibrary.getFacet(AutoListArmorProfFacet.class); private AutoListShieldProfFacet shieldProfListFacet = FacetLibrary.getFacet(AutoListShieldProfFacet.class); private AutoListWeaponProfFacet alWeaponProfFacet = FacetLibrary.getFacet(AutoListWeaponProfFacet.class); private RegionFacet regionFacet = FacetLibrary.getFacet(RegionFacet.class); private NoteItemFacet noteItemFacet = FacetLibrary.getFacet(NoteItemFacet.class); private GlobalAddedSkillCostFacet globalAddedSkillCostFacet = FacetLibrary .getFacet(GlobalAddedSkillCostFacet.class); private LocalAddedSkillCostFacet localAddedSkillCostFacet = FacetLibrary.getFacet(LocalAddedSkillCostFacet.class); private PreviewSheetFacet previewSheetFacet = FacetLibrary.getFacet(PreviewSheetFacet.class); private SkillFilterFacet skillFilterFacet = FacetLibrary.getFacet(SkillFilterFacet.class); //The following facets are pure delegation (no exceptions) - could be considered "complete" private AddedTemplateFacet addedTemplateFacet = FacetLibrary.getFacet(AddedTemplateFacet.class); private BonusWeaponProfFacet wpBonusFacet = FacetLibrary.getFacet(BonusWeaponProfFacet.class); private ClassSpellListFacet classSpellListFacet = FacetLibrary.getFacet(ClassSpellListFacet.class); private DomainSpellCountFacet domainSpellCountFacet = FacetLibrary.getFacet(DomainSpellCountFacet.class); private LegalDeityFacet legalDeityFacet = FacetLibrary.getFacet(LegalDeityFacet.class); private GoldFacet goldFacet = FacetLibrary.getFacet(GoldFacet.class); private MonsterCSkillFacet monCSkillFacet = FacetLibrary.getFacet(MonsterCSkillFacet.class); private NonAbilityFacet nonAbilityFacet = FacetLibrary.getFacet(NonAbilityFacet.class); private QualifyFacet qualifyFacet = FacetLibrary.getFacet(QualifyFacet.class); private SkillOutputOrderFacet skillOutputOrderFacet = FacetLibrary.getFacet(SkillOutputOrderFacet.class); private SkillPoolFacet skillPoolFacet = FacetLibrary.getFacet(SkillPoolFacet.class); private SkillRankFacet skillRankFacet = FacetLibrary.getFacet(SkillRankFacet.class); private StartingLanguageFacet startingLangFacet = FacetLibrary.getFacet(StartingLanguageFacet.class); private StatCalcFacet statCalcFacet = FacetLibrary.getFacet(StatCalcFacet.class); private StatLockFacet statLockFacet = FacetLibrary.getFacet(StatLockFacet.class); private StatValueFacet statValueFacet = FacetLibrary.getFacet(StatValueFacet.class); private SubClassFacet subClassFacet = FacetLibrary.getFacet(SubClassFacet.class); private SubstitutionClassFacet substitutionClassFacet = FacetLibrary.getFacet(SubstitutionClassFacet.class); private UnlockedStatFacet unlockedStatFacet = FacetLibrary.getFacet(UnlockedStatFacet.class); private NonStatStatFacet nonStatStatFacet = FacetLibrary.getFacet(NonStatStatFacet.class); private NonStatToStatFacet nonStatToStatFacet = FacetLibrary.getFacet(NonStatToStatFacet.class); private TemplateFeatFacet templateFeatFacet = FacetLibrary.getFacet(TemplateFeatFacet.class); private SavedAbilitiesFacet svAbilityFacet = FacetLibrary.getFacet(SavedAbilitiesFacet.class); /* * Note "minimal" here means getDirty is allowed on a set, it may be used in * clone(), but no other calls are made in any methods. Also any delegation * method is not used elsewhere in PlayerCharacter except clone() or an * otherwise pure delegation method. Also allowed is pure binary connections * between two facets in a get (A && B) */ //The following facets are "minimal" delegation private XPFacet xpFacet = FacetLibrary.getFacet(XPFacet.class); private XPTableFacet xpTableFacet = FacetLibrary.getFacet(XPTableFacet.class); //The following are model facets that are only set or getCDOMObjectList or getBonusContainer (nearly isolated) private AlignmentFacet alignmentFacet = FacetLibrary.getFacet(AlignmentFacet.class); private CheckFacet checkFacet = FacetLibrary.getFacet(CheckFacet.class); private CompanionModFacet companionModFacet = FacetLibrary.getFacet(CompanionModFacet.class); private CampaignFacet campaignFacet = FacetLibrary.getFacet(CampaignFacet.class); private ExpandedCampaignFacet expandedCampaignFacet = FacetLibrary.getFacet(ExpandedCampaignFacet.class); private AgeSetFacet ageSetFacet = FacetLibrary.getFacet(AgeSetFacet.class); //The following are other facets private DomainFacet domainFacet = FacetLibrary.getFacet(DomainFacet.class); private DomainInputFacet domainInputFacet = FacetLibrary.getFacet(DomainInputFacet.class); private TemplateFacet templateFacet = FacetLibrary.getFacet(TemplateFacet.class); private TemplateInputFacet templateInputFacet = FacetLibrary.getFacet(TemplateInputFacet.class); private DeityFacet deityFacet = FacetLibrary.getFacet(DeityFacet.class); private RaceFacet raceFacet = FacetLibrary.getFacet(RaceFacet.class); private RaceInputFacet raceInputFacet = FacetLibrary.getFacet(RaceInputFacet.class); private StatFacet statFacet = FacetLibrary.getFacet(StatFacet.class); private StatBonusFacet statBonusFacet = FacetLibrary.getFacet(StatBonusFacet.class); private CheckBonusFacet checkBonusFacet = FacetLibrary.getFacet(CheckBonusFacet.class); private SkillFacet skillFacet = FacetLibrary.getFacet(SkillFacet.class); private ClassFacet classFacet = FacetLibrary.getFacet(ClassFacet.class); private BioSetFacet bioSetFacet = FacetLibrary.getFacet(BioSetFacet.class); private UserEquipmentFacet userEquipmentFacet = FacetLibrary.getFacet(UserEquipmentFacet.class); private EquipmentFacet equipmentFacet = FacetLibrary.getFacet(EquipmentFacet.class); private EquippedEquipmentFacet equippedFacet = FacetLibrary.getFacet(EquippedEquipmentFacet.class); private SourcedEquipmentFacet activeEquipmentFacet = FacetLibrary.getFacet(SourcedEquipmentFacet.class); private ConditionallyGrantedAbilityFacet cabFacet = FacetLibrary.getFacet(ConditionallyGrantedAbilityFacet.class); private ConditionallyGrantedKnownSpellFacet cKnSpellFacet = FacetLibrary.getFacet(ConditionallyGrantedKnownSpellFacet.class); private ConditionallyGrantedAvailableSpellFacet cAvSpellFacet = FacetLibrary.getFacet(ConditionallyGrantedAvailableSpellFacet.class); private ConditionalAbilityFacet conditionalFacet = FacetLibrary.getFacet(ConditionalAbilityFacet.class); private GrantedAbilityFacet grantedAbilityFacet = FacetLibrary.getFacet(GrantedAbilityFacet.class); private DirectAbilityFacet directAbilityFacet = FacetLibrary.getFacet(DirectAbilityFacet.class); private KitFacet kitFacet = FacetLibrary.getFacet(KitFacet.class); private ArmorProfProviderFacet armorProfFacet = FacetLibrary.getFacet(ArmorProfProviderFacet.class); private ShieldProfProviderFacet shieldProfFacet = FacetLibrary.getFacet(ShieldProfProviderFacet.class); private CharacterSpellResistanceFacet srFacet = FacetLibrary.getFacet(CharacterSpellResistanceFacet.class); private WeaponProfModelFacet weaponProfFacet = FacetLibrary.getFacet(WeaponProfModelFacet.class); private MasterFacet masterFacet = FacetLibrary.getFacet(MasterFacet.class); private AutoEquipmentListFacet autoListEquipmentFacet = FacetLibrary.getFacet(AutoEquipmentListFacet.class); private FollowerFacet followerFacet = FacetLibrary.getFacet(FollowerFacet.class); private LanguageFacet languageFacet = FacetLibrary.getFacet(LanguageFacet.class); private UserSpecialAbilityFacet userSpecialAbilityFacet = FacetLibrary.getFacet(UserSpecialAbilityFacet.class); private SpecialAbilityFacet specialAbilityFacet = FacetLibrary.getFacet(SpecialAbilityFacet.class); private PrimaryWeaponFacet primaryWeaponFacet = FacetLibrary.getFacet(PrimaryWeaponFacet.class); private SecondaryWeaponFacet secondaryWeaponFacet = FacetLibrary.getFacet(SecondaryWeaponFacet.class); private AutoLanguageGrantedFacet condLangFacet = FacetLibrary.getFacet(AutoLanguageGrantedFacet.class); private SkillCostFacet skillCostFacet = FacetLibrary.getFacet(SkillCostFacet.class); private ProhibitedSchoolFacet prohibitedSchoolFacet = FacetLibrary.getFacet(ProhibitedSchoolFacet.class); private SpellProhibitorFacet spellProhibitorFacet = FacetLibrary.getFacet(SpellProhibitorFacet.class); private GlobalModifierFacet globalModifierFacet = FacetLibrary.getFacet(GlobalModifierFacet.class); private ObjectCache cache = new ObjectCache(); private AssociationSupport assocSupt = new AssociationSupport(); private BonusManager bonusManager = new BonusManager(this); private BonusChangeFacet bonusChangeFacet = FacetLibrary.getFacet(BonusChangeFacet.class); private EquipSetFacet equipSetFacet = FacetLibrary.getFacet(EquipSetFacet.class); private HitPointFacet hitPointFacet = FacetLibrary.getFacet(HitPointFacet.class); private KnownSpellFacet knownSpellFacet = FacetLibrary.getFacet(KnownSpellFacet.class); private LevelFacet levelFacet = FacetLibrary.getFacet(LevelFacet.class); private LevelTableFacet levelTableFacet = FacetLibrary.getFacet(LevelTableFacet.class); private SizeFacet sizeFacet = FacetLibrary.getFacet(SizeFacet.class); private FactFacet factFacet = FacetLibrary.getFacet(FactFacet.class); private FavoredClassFacet favClassFacet = FacetLibrary.getFacet(FavoredClassFacet.class); private VariableFacet variableFacet = FacetLibrary.getFacet(VariableFacet.class); private FollowerLimitFacet followerLimitFacet = FacetLibrary.getFacet(FollowerLimitFacet.class); private AvailableSpellFacet availSpellFacet = FacetLibrary.getFacet(AvailableSpellFacet.class); private MovementResultFacet moveResultFacet = FacetLibrary.getFacet(MovementResultFacet.class); private AutoEquipmentFacet autoEquipFacet = FacetLibrary.getFacet(AutoEquipmentFacet.class); private SpellBookFacet spellBookFacet = FacetLibrary.getFacet(SpellBookFacet.class); private LoadFacet loadFacet = FacetLibrary.getFacet(LoadFacet.class); private AppliedBonusFacet appliedBonusFacet = FacetLibrary.getFacet(AppliedBonusFacet.class); private AddedBonusFacet addedBonusFacet = FacetLibrary.getFacet(AddedBonusFacet.class); private SaveableBonusFacet saveableBonusFacet = FacetLibrary.getFacet(SaveableBonusFacet.class); private SpellSupportFacet spellSupportFacet = FacetLibrary.getFacet(SpellSupportFacet.class); private AgeFacet ageFacet = FacetLibrary.getFacet(AgeFacet.class); private ActiveSpellsFacet activeSpellsFacet = FacetLibrary.getFacet(ActiveSpellsFacet.class); private SpellListFacet spellListFacet = FacetLibrary.getFacet(SpellListFacet.class); private ChangeProfFacet changeProfFacet = FacetLibrary.getFacet(ChangeProfFacet.class); private TargetTrackingFacet astocnasFacet = FacetLibrary.getFacet(TargetTrackingFacet.class); private PlayerCharacterTrackingFacet trackingFacet = FacetLibrary.getFacet(PlayerCharacterTrackingFacet.class); private PortraitThumbnailRectFacet portraitThumbnailRectFacet = FacetLibrary .getFacet(PortraitThumbnailRectFacet.class); private BonusSkillRankChangeFacet bonusSkillRankChangeFacet = FacetLibrary.getFacet(BonusSkillRankChangeFacet.class); private LevelInfoFacet levelInfoFacet = FacetLibrary.getFacet(LevelInfoFacet.class); private SolverManagerFacet solverManagerFacet = FacetLibrary.getFacet(SolverManagerFacet.class); private SolverFactoryFacet solverFactoryFacet = FacetLibrary.getFacet(SolverFactoryFacet.class); private FormulaSetupFacet formulaSetupFacet = FacetLibrary.getFacet(FormulaSetupFacet.class); private ResultFacet resultFacet = FacetLibrary.getFacet(ResultFacet.class); private ScopeFacet scopeFacet = FacetLibrary.getFacet(ScopeFacet.class); private VariableStoreFacet variableStoreFacet = FacetLibrary.getFacet(VariableStoreFacet.class); private ClassSource defaultDomainSource; private Map<String, Integer> autoEquipOutputOrderCache = new HashMap<>(); // Temporary Bonuses private List<Equipment> tempBonusItemList = new ArrayList<>(); private String calcEquipSetId = EquipSet.DEFAULT_SET_PATH; //$NON-NLS-1$ private String descriptionLst = "EMPTY"; //$NON-NLS-1$ // whether to add auto known spells each level private boolean autoKnownSpells = true; // whether higher level spell slots should be used for lower levels private boolean useHigherKnownSlots = SettingsHandler.isUseHigherLevelSlotsDefault(); private boolean useHigherPreppedSlots = SettingsHandler.isUseHigherLevelSlotsDefault(); // should we also load companions on master load? private boolean autoLoadCompanion = false; // Should we sort the gear automatically? private boolean autoSortGear = true; // Should we resize the gear automatically? private boolean autoResize = PCGenSettings.getInstance().getBoolean(PCGenSettings.OPTION_AUTO_RESIZE_EQUIP, true); // output sheet locations private String outputSheetHTML = Constants.EMPTY_STRING; private String outputSheetPDF = Constants.EMPTY_STRING; private boolean[] ageSetKitSelections = new boolean[Constants.NUMBER_OF_AGESET_KIT_SELECTIONS]; private boolean dirtyFlag = false; private int serial = 0; private boolean importing = false; // Should temp mods/bonuses be used/saved? private boolean useTempMods = true; // null is <none selected> private int costPool = 0; private int currentEquipSetNumber = 0; // pool of stats allowed to distribute private int poolAmount = 0; // order in which the skills will be output. private SkillsOutputOrder skillsOutputOrder = SkillsOutputOrder.NAME_ASC; private int spellLevelTemp = 0; private VariableProcessor variableProcessor; // used by point buy. Total number of points for method, not points // remaining private int pointBuyPoints = -1; private boolean processLevelAbilities = true; private boolean allowInteraction = true; /** * This map stores any user bonuses (entered through the GUI) to the * corresponding ability pool. */ private Map<Category<Ability>, BigDecimal> theUserPoolBonuses = null; // ///////////////////////////////////// // operations private CNAbility bonusLanguageAbility = CNAbilityFactory.getCNAbility(AbilityCategory.LANGBONUS, Nature.VIRTUAL, Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject( Ability.class, AbilityCategory.LANGBONUS, "*LANGBONUS")); private CodeControl controller; /** * Constructor. */ public PlayerCharacter() { this(Collections.emptyList()); } /** * Constructor. * * @param loadedCampaigns The currently loaded campaign objects. */ public PlayerCharacter(Collection<Campaign> loadedCampaigns) { LoadContext context = Globals.getContext(); id = CharID.getID(context.getDataSetID()); doFormulaSetup(context); display = new CharacterDisplay(id); SA_TO_STRING_PROC = new SAtoStringProcessor(this); SA_PROC = new SAProcessor(this); trackingFacet.associatePlayerCharacter(id, this); variableProcessor = new VariableProcessorPC(this); for (int i = 0; i < Constants.NUMBER_OF_AGESET_KIT_SELECTIONS; i++) { ageSetKitSelections[i] = false; } AbstractReferenceContext refContext = context.getReferenceContext(); GlobalModifiers gm = refContext.constructNowIfNecessary(GlobalModifiers.class, "Global Modifiers"); globalModifierFacet.set(id, gm); controller = refContext.constructNowIfNecessary(CodeControl.class, "Controller"); //Do BioSet first, since required by Race bioSetFacet.set(id, SettingsHandler.getGame().getBioSet()); //Set Race before Stat/Check due to Default object in Pathfinder/RSRD setRace(Globals.s_EMPTYRACE); statFacet.addAll(id, refContext.getOrderSortedCDOMObjects(PCStat.class)); checkFacet.addAll(id, refContext.getOrderSortedCDOMObjects(PCCheck.class)); campaignFacet.addAll(id, loadedCampaigns); setGold(new BigDecimal(0)); setXPTable(SettingsHandler.getGame().getDefaultXPTableName()); setCharacterType(SettingsHandler.getGame().getDefaultCharacterType()); setPreviewSheet(SettingsHandler.getGame().getDefaultPreviewSheet()); setName(Constants.EMPTY_STRING); setUserPoolBonus(AbilityCategory.FEAT, BigDecimal.ZERO); rollStats(SettingsHandler.getGame().getRollMethod()); addSpellBook(new SpellBook(Globals.getDefaultSpellBook(), SpellBook.TYPE_KNOWN_SPELLS)); addSpellBook(new SpellBook(Constants.INNATE_SPELL_BOOK_NAME, SpellBook.TYPE_INNATE_SPELLS)); } /** * Set the Weapon proficiency of one piece of Equipment to the same as the * Proficiency in another piece of Equipment. For some bizarre reason, as * well as setting the proficiency, this zeros out the Weight and cost of * the equipment. * * @param equip the Weapon to get the proficiency from * @param eqm the weapon to set the proficiency in */ private static void setProf(final Equipment equip, final Equipment eqm) { eqm.put(ObjectKey.WEAPON_PROF, equip.get(ObjectKey.WEAPON_PROF)); // In case this is used somewhere it shouldn't be used, // set weight and cost to 0 eqm.put(ObjectKey.WEIGHT, BigDecimal.ZERO); eqm.put(ObjectKey.CURRENT_COST, BigDecimal.ZERO); } private void doFormulaSetup(LoadContext context) { SplitFormulaSetup formulaSetup = formulaSetupFacet.get(id.getDatasetID()); MonitorableVariableStore varStore = new MonitorableVariableStore(); IndividualSetup mySetup = new IndividualSetup(formulaSetup, "Global", varStore); scopeFacet.set(id, mySetup.getInstanceFactory()); variableStoreFacet.set(id, varStore); SolverFactory solverFactory = solverFactoryFacet.get(id.getDatasetID()); solverManagerFacet.set(id, new AggressiveSolverManager(mySetup.getFormulaManager(), context.getVariableContext().getManagerFactory(), solverFactory, varStore)); } @Override public String toString() { return "PlayerCharacter [name=" + getName() + " @ " + getFileName() + " serial=" + serial + "]"; } public void setPCAttribute(final NumericPCAttribute attr, final int value) { boolean didChange = false; switch(attr) { case WEIGHT: didChange = weightFacet.setWeight(id, value); break; case AGE: didChange = ageFacet.set(id, value); break; } if (didChange) { setDirty(true); if (attr.shouldRecalcActiveBonuses()) { calcActiveBonuses(); } } } /** * Sets player character information * * @param attr which attribute to set * @param value the value to set it to */ public void setPCAttribute(final StringPCAttribute attr, final String value) { setStringFor(attr.getStringKey(), value); } /** * Set the current EquipSet that is used to Bonus/Equip calculations. * * @param eqSetId The equipSet to be used for Bonus Calculations and output */ public void setCalcEquipSetId(final String eqSetId) { if (calcEquipSetId != eqSetId) { calcEquipSetId = eqSetId; EquipSet equipSet = getEquipSetByIdPath(eqSetId); if (equipSet != null) { setCurrentEquipSetName(equipSet.getName()); } setDirty(true); } } /** * Get the id for the equipment set being used for calculation. * * @return id */ public String getCalcEquipSetId() { if (equipSetFacet.isEmpty(id)) { return calcEquipSetId; } if (getEquipSetByIdPath(calcEquipSetId) == null) { // PC does not have that equipset ID // so we need to find one they do have for (EquipSet eSet : equipSetFacet.getSet(id)) { if (eSet.getParentIdPath().equals(Constants.EQUIP_SET_ROOT_ID)) { calcEquipSetId = eSet.getIdPath(); return calcEquipSetId; } } } return calcEquipSetId; } /** * Set's current equipmentList to selected output EquipSet then loops * through all the equipment and sets the correct status of each (equipped, * carried, etc). */ public void setCalcEquipmentList() { setCalcEquipmentList(false); } /** * Set's current equipmentList to selected output EquipSet then loops * through all the equipment and sets the correct status of each (equipped, * carried, etc). Boolean parameter useTempBonuses controls whether or * not the temporary bonuses associated with equipment are applied. * * @param useTempBonuses whether to apply Temporary bonuses from equipment. */ public void setCalcEquipmentList(final boolean useTempBonuses) { // First we get the EquipSet that is going to be used // to calculate everything from final String calcId = getCalcEquipSetId(); final EquipSet eSet = getEquipSetByIdPath(calcId); if (eSet == null) { if (Logging.isDebugMode()) { Logging .debugPrint("No EquipSet has been selected for calculations yet."); //$NON-NLS-1$ } return; } // set PC's equipmentList to new one /* * TODO This "global reset" directly followed by testing in the * EquipSets and re-adding items as local equipment is something that * needs to be cleaned up */ equipmentFacet.removeAll(id); // get all the PC's EquipSet's final List<EquipSet> pcEquipSetList = new ArrayList<>(getEquipSet()); if (pcEquipSetList.isEmpty()) { equippedFacet.reset(id); return; } // make sure EquipSet's are in sorted order // (important for Containers contents) Collections.sort(pcEquipSetList); // loop through all the EquipSet's and create equipment // then set status to equipped and add to PC's equipment list for (EquipSet es : pcEquipSetList) { if (es.getItem() == null || !es.isPartOf(calcId)) { continue; } es.equipItem(this); es.addNoteToItem(); addLocalEquipment(es.getItem()); } // loop through all equipment and make sure that // containers contents are updated for (Equipment eq : getEquipmentSet()) { if (eq.isContainer()) { eq.updateContainerContentsString(this); } // also make sure the masterList output order is // preserved as this equipmentList is a modified // clone of the original final Equipment anEquip = getEquipmentNamed(eq.getName()); if (anEquip != null) { eq.setOutputIndex(anEquip.getOutputIndex()); } } // if temporary bonuses, read the bonus equipList if (useTempBonuses) { for (Equipment eq : tempBonusItemList) { // make sure that this EquipSet is the one // this temporary bonus item comes from // to make sure we keep them together final Equipment anEquip = getEquipmentNamed(eq.getName(), getEquipmentSet()); if (anEquip == null) { continue; } eq.setQty(anEquip.getQty()); eq.setNumberCarried(anEquip.getCarried()); if (anEquip.isEquipped()) { if (eq.isWeapon()) { eq.put(IntegerKey.SLOTS, 0); eq.put(ObjectKey.CURRENT_COST, BigDecimal.ZERO); eq.put(ObjectKey.WEIGHT, BigDecimal.ZERO); eq.setLocation(anEquip.getLocation()); } else { // replace the orig item with the bonus item eq.setLocation(anEquip.getLocation()); removeLocalEquipment(anEquip); anEquip.setIsEquipped(false, this); anEquip.setLocation(EquipmentLocation.NOT_CARRIED); anEquip.setNumberCarried(0.0f); } eq.setIsEquipped(true, this); eq.setNumberEquipped(1); } else { eq.put(ObjectKey.CURRENT_COST, BigDecimal.ZERO); eq.put(ObjectKey.WEIGHT, BigDecimal.ZERO); eq.setLocation(EquipmentLocation.EQUIPPED_TEMPBONUS); eq.setIsEquipped(false, this); } // Adding this type to be correctly treated by Merge eq.addType(Type.TEMPORARY); addLocalEquipment(eq); } } // all done! equippedFacet.reset(id); } /** * Apply the bonus from a follower to the master pc. */ public void setCalcFollowerBonus() { setDirty(true); for (Follower aF : getFollowerList()) { final CompanionList cList = aF.getType(); final String rType = cList.getKeyName(); final Race fRace = aF.getRace(); for (CompanionMod cm : Globals.getContext().getReferenceContext().getManufacturer( CompanionMod.class, cList).getAllObjects()) { final String aType = cm.getType(); if (aType.equalsIgnoreCase(rType) && cm.appliesToRace(fRace)) { // Found race and type of follower // so add bonus to the master companionModFacet.add(id, cm); } } } } /** * Get a class, represented by a given key, from among those possessed by this pc. * * @param key the class's key * @return PCClass */ public PCClass getClassKeyed(final String key) { for (PCClass aClass : getClassSet()) { if (aClass.getKeyName().equalsIgnoreCase(key)) { return aClass; } } return null; } /** * Get the class list. * * @return classList */ public List<PCClass> getClassList() { /* * TODO This is a discussion we have to have about where items are sorted */ return new ArrayList<>(getClassSet()); } /** * Gets the Set of PCClass objects for this Character. * @return a set of PCClass objects */ public Set<PCClass> getClassSet() { return classFacet.getSet(id); } /** * Set the cost pool, which is the number of points the character has spent. * * @param i the number of points spent */ public void setCostPool(final int i) { costPool = i; } /** * Get the cost pool, which is the number of points the character has spent. * * @return costPool */ public int getCostPool() { return costPool; } /** * Set the current equipment set name. * * @param aName the name of the new current equipment set */ public void setCurrentEquipSetName(final String aName) { setStringFor(PCStringKey.CURRENT_EQUIP_SET_NAME, aName); } /** * Get the deity. * * @return deity */ public Deity getDeity() { return deityFacet.get(id); } /** * Selector. * * @return description lst */ public String getDescriptionLst() { return descriptionLst; } /** * Sets the character changed since last save. * NB: This is not a 'safe' call - its use should be considered carefully and in * particular it should not be called from a method used as part of PlayerCharacter * cloning as this can mean conditional abilities get dropped when they are actually * qualified for, just not at that point in the clone. * * @param dirtyState the new "dirty" value (may be false to indicate no change) */ public void setDirty(final boolean dirtyState) { if (dirtyState) { serial++; cache = new ObjectCache(); variableProcessor.setSerial(serial); cabFacet.update(id); cAvSpellFacet.update(id); cKnSpellFacet.update(id); condLangFacet.update(id); bonusSkillRankChangeFacet.reset(id); } dirtyFlag = dirtyState; } /** * Gets whether the character has been changed since last saved. * * @return true if dirty */ public boolean isDirty() { return dirtyFlag; } /** * Returns the serial for the instance - every time something changes the * serial is incremented. Use to detect change in PlayerCharacter. * * @return serial */ public int getSerial() { return serial; } /** * Get the list of equipment sets. * * @return List */ private Collection<EquipSet> getEquipSet() { return equipSetFacet.getSet(id); } /** * Get the equipment set indexed by path. * * @param path the "path" of the equipSet to return * @return EquipSet */ public EquipSet getEquipSetByIdPath(final String path) { return equipSetFacet.getEquipSetByIdPath(id, path); } /** * Get the current equipment set number. * * @return equipSet number */ public int getEquipSetNumber() { return currentEquipSetNumber; } /** * Get equipment set. * * @return equipment set */ private Set<Equipment> getEquipmentSet() { return equipmentFacet.getSet(id); } /** * Get the character's "equipped" equipment. * @return a set of the "equipped" equipment */ public Set<Equipment> getEquippedEquipmentSet() { return equippedFacet.getSet(id); } /** * Retrieves a list of the character's equipment in output order. This is in * ascending order of the equipment's outputIndex field. If multiple items * of equipment have the same outputIndex they will be ordered by name. Note * hidden items (outputIndex = -1) are not included in this list. * * @return An ArrayList of the equipment objects in output order. */ public List<Equipment> getEquipmentListInOutputOrder() { return sortEquipmentList(getEquipmentSet(), Constants.MERGE_ALL); } /** * Retrieves a list of the character's equipment in output order. This is in * ascending order of the equipment's outputIndex field. If multiple items * of equipment have the same outputIndex they will be ordered by name. Note * hidden items (outputIndex = -1) are not included in this list. * * Deals with merge as well. See the Constants package for acceptable values * of merge . * * @param merge controls how much merging is done. * * @return An ArrayList of the equipment objects in output order. */ public List<Equipment> getEquipmentListInOutputOrder(final int merge) { return sortEquipmentList(getEquipmentSet(), merge); } /** * Get the master list of equipment. * * @return equipment master list */ public List<Equipment> getEquipmentMasterList() { Set<Equipment> set = userEquipmentFacet.getSet(id); final List<Equipment> aList = new ArrayList<>(set); aList.addAll(autoListEquipmentFacet.getSet(id)); aList.addAll(autoEquipFacet.getAutoEquipment(id)); return aList; } /** * Search for a piece of equipment in the specified list by name. * * TODO - This does not belong in PlayerCharacter. Move to Equipment if * needed. * * TODO - This probably won't work with i18n. Should always search by key. * * @param aString * The name of the equipment. * @param aList * The Collection of equipment to search in. * * @return The <tt>Equipment</tt> object or <tt>null</tt> */ private static Equipment getEquipmentNamed(final String aString, final Collection<Equipment> aList) { Equipment match = null; for (Equipment eq : aList) { if (aString.equalsIgnoreCase(eq.getName())) { match = eq; } } return match; } /** * Search among the PCs equipment for a named piece of equipment. * @param name The name of the piece of equipment. * @return null or the equipment named. */ public Equipment getEquipmentNamed(final String name) { return getEquipmentNamed(name, getEquipmentMasterList()); } /** * Set the characters eye colour. * * @param aString * the colour of their eyes */ public void setEyeColor(final String aString) { setStringFor(PCStringKey.EYECOLOR, aString); } /** * Get a number that represents the number of feats added to this character * by BONUS statements. * * @return the number of feats added by bonus statements */ private double getBonusFeatPool() { String aString = Globals.getBonusFeatString(); final StringTokenizer aTok = new StringTokenizer(aString, Constants.PIPE, false); final int startLevel = Integer.parseInt(aTok.nextToken()); final int rangeLevel = Integer.parseInt(aTok.nextToken()); double pool = getTotalBonusTo("FEAT", "POOL"); double pcpool = getTotalBonusTo("FEAT", "PCPOOL"); double mpool = getTotalBonusTo("FEAT", "MONSTERPOOL"); double bonus = getTotalBonusTo("ABILITYPOOL", "FEAT"); double classLvlBonus = getNumFeatsFromLevels(); if (Logging.isDebugMode()) { Logging.debugPrint(""); //$NON-NLS-1$ Logging.debugPrint("=============="); //$NON-NLS-1$ Logging.debugPrint("level " + this.totalNonMonsterLevels()); //$NON-NLS-1$ Logging.debugPrint("POOL: " + pool); //$NON-NLS-1$ Logging.debugPrint("PCPOOL: " + pcpool); //$NON-NLS-1$ Logging.debugPrint("MPOOL: " + mpool); //$NON-NLS-1$ Logging.debugPrint("APOOL: " + bonus); //$NON-NLS-1$ Logging.debugPrint("LVLBONUS: " + classLvlBonus); //$NON-NLS-1$ } double startAdjust = rangeLevel == 0 ? 0 : startLevel / rangeLevel; double nonMonsterAdjustment = this.totalNonMonsterLevels() >= startLevel ? 1.0d + pcpool - startAdjust : pcpool; pool += CoreUtility.epsilonFloor(nonMonsterAdjustment); pool += CoreUtility.epsilonFloor(mpool); pool += CoreUtility.epsilonFloor(bonus); pool += CoreUtility.epsilonFloor(classLvlBonus); if (Logging.isDebugMode()) { Logging.debugPrint(""); //$NON-NLS-1$ Logging.debugPrint("Total Bonus: " + pool); //$NON-NLS-1$ Logging.debugPrint("=============="); //$NON-NLS-1$ Logging.debugPrint(""); //$NON-NLS-1$ } return pool; } /** * Calculates the number of feats that should be granted as a result of LEVELPERFEAT * entries in classes that the character has levels in. Stacking rules based on * LEVELTYPE are applied as part of this calculation. * * @return the number of feats granted */ double getNumFeatsFromLevels() { Map<String, Double> featByLevelType = new HashMap<>(); for (PCClass pcClass : getClassSet()) { int lvlPerFeat = pcClass.getSafe(IntegerKey.LEVELS_PER_FEAT); if (lvlPerFeat != 0) { double bonus = (double) getLevel(pcClass) / lvlPerFeat; Double existing = featByLevelType.get(pcClass.get(StringKey.LEVEL_TYPE)); if (existing == null) { existing = 0.0d; } existing += bonus; featByLevelType.put(pcClass.get(StringKey.LEVEL_TYPE), existing); } } double bonus = 0.0d; for (final Map.Entry<String, Double> stringDoubleEntry : featByLevelType.entrySet()) { Double existing = stringDoubleEntry.getValue(); bonus += CoreUtility.epsilonFloor(existing); } return bonus; } /** * Checks whether a PC is allowed to level up. A PC is not allowed to level * up if the "Enforce Spending" option is set and he still has unallocated * skill points and/or feat slots remaining. This can be used to enforce * correct spending of these resources when creating high-level multiclass * characters. * * @return true if the PC can level up */ public boolean canLevelUp() { return !SettingsHandler.getEnforceSpendingBeforeLevelUp() || (getSkillPoints() <= 0 && getRemainingFeatPoolPoints() <= 0); } /** * Sets the filename of the character. * * @param newFileName the name of the file this character will be saved in */ public void setFileName(final String newFileName) { setStringFor(PCStringKey.FILE_NAME, newFileName); } /** * Gets the filename of the character. * * @return file name of character */ public String getFileName() { return getSafeStringFor(PCStringKey.FILE_NAME); } /** * Returns the followers associated with this character. * * @return A <tt>Set</tt> of <tt>Follower</tt> objects. */ public Collection<Follower> getFollowerList() { return followerFacet.getSet(id); } /** * Sets the character's gender. * * <p> * The gender will only be changed if the character does not have a template * that locks the character's gender. * * @param g * A gender to try and set. */ public void setGender(final Gender g) { if (genderFacet.getGender(id) != g) { genderFacet.setGender(id, g); setDirty(true); } } /** * Sets the character's wealth. * * <p> * Gold here is used as a character's total purchase power not actual gold * pieces. * * @param aString * A String gold amount. TODO - Do this parsing elsewhere. */ public void setGold(final String aString) { BigDecimal gold = new BigDecimal(aString); setGold(gold); } /** * Sets the character's wealth. * * <p> * Gold here is used as a character's total purchase power not actual gold * pieces. * * @param amt * A gold amount. */ public void setGold(final BigDecimal amt) { if (amt == null) { return; } // The equality comparison in AbstractItemFacet doesn't work on BigDecimal, need to use compareTo BigDecimal oldAmt = goldFacet.get(id); if (oldAmt == null || amt.compareTo(oldAmt) != 0) { goldFacet.set(id, amt); setDirty(true); } } /** * Returns the character's total wealth. * * @see pcgen.core.PlayerCharacter#setGold(String) * * @return A <tt>BigDecimal</tt> value for the character's wealth. */ public BigDecimal getGold() { BigDecimal g = goldFacet.get(id); return (g == null) ? BigDecimal.ZERO : g; } /** * Sets the character's handedness. * * @param h A handedness to try and set. */ public void setHanded(final Handed h) { if (handedFacet.setHanded(id, h)) { setDirty(true); } } /** * Sets the character's height in inches. * * @param i * A height in inches. * * TODO - This should be a double value stored in CM */ public void setHeight(final int i) { if (heightFacet.setHeight(id, i)) { setDirty(true); } } /** * Marks the character as being in the process of being loaded. * * <p> * This information is used to prevent the system from trying to calculate * values on partial information or values that should be set from the saved * character. * * <p> * TODO - This is pretty dangerous. * * @param newIsImporting * <tt>true</tt> to mark the character as being imported. */ public void setImporting(final boolean newIsImporting) { this.importing = newIsImporting; } /** * Gets the character's list of languages. * * @return An unmodifiable language set. */ public Set<Language> getLanguageSet() { return languageFacet.getSet(id); } /** * This method returns the effective level of this character for purposes of * applying companion mods to a companion of the specified type. * <p> * <b>Note</b>: This whole structure is kind of messed up since nothing * enforces that a companion mod of a given type always looks at the same * variable (either Class or Variable). Note it seems that this used to * be driven off types but now it's driven from a list of companion mods * but the java doc has not been updated. * * @param compList * A list of companionMods to get level for * @return The effective level for this companion type */ public int getEffectiveCompanionLevel(final CompanionList compList) { for (CompanionMod cMod : Globals.getContext().getReferenceContext().getManufacturer( CompanionMod.class, compList).getAllObjects()) { Map<String, Integer> varmap = cMod.getMapFor(MapKey.APPLIED_VARIABLE); for (final String varName : varmap.keySet()) { final int lvl = this.getVariableValue(varName, Constants.EMPTY_STRING).intValue(); if (lvl > 0) { return lvl; } } Map<CDOMSingleRef<? extends PCClass>, Integer> ac = cMod.getMapFor(MapKey.APPLIED_CLASS); for (Map.Entry<CDOMSingleRef<? extends PCClass>, Integer> me : ac.entrySet()) { PCClass pcclass = me.getKey().get(); String key = pcclass.getKeyName(); int lvl = getLevel(getClassKeyed(key)); if (lvl > 0) { return lvl; } } } return 0; } /** * Set the master for this object also set the level dependent stats based * on the masters level and info contained in the companionModList Array * such as HitDie, SR, BONUS, SA, etc. * * @param aM * The master to be set. */ public void setMaster(final Follower aM) { masterFacet.set(id, aM); final PlayerCharacter mPC = getMasterPC(); if (mPC == null) { return; } // make sure masters Name and fileName are correct if (!aM.getFileName().equals(mPC.getFileName())) { aM.setFileName(mPC.getFileName()); setDirty(true); } if (!aM.getName().equals(mPC.getName())) { aM.setName(mPC.getName()); setDirty(true); } // Get total wizard + sorcerer levels as they stack like a mother int mTotalLevel = 0; int addHD = 0; for (PCClass mClass : mPC.getClassSet()) { boolean found = false; for (CompanionMod cMod : Globals.getContext().getReferenceContext().getManufacturer( CompanionMod.class, aM.getType()).getAllObjects()) { if ((cMod.getLevelApplied(mClass) > 0) && !found) { mTotalLevel += getLevel(mClass); found = true; } } } List<CompanionMod> newCompanionMods = new ArrayList<>(); // Clear the companionModList so we can add everything to it Collection<CompanionMod> oldCompanionMods = companionModFacet.removeAll(id); for (CompanionMod cMod : Globals.getContext().getReferenceContext().getManufacturer( CompanionMod.class, aM.getType()).getAllObjects()) { // Check all the masters classes for (PCClass mClass : mPC.getClassSet()) { final int mLev = mPC.getLevel(mClass) + aM.getAdjustment(); final int compLev = cMod.getLevelApplied(mClass); if (compLev < 0) { continue; } // This CompanionMod must be for this Class // and for the correct level or lower if ((compLev <= mLev) || (compLev <= mTotalLevel)) { if (cMod.qualifies(this, cMod)) { if (!oldCompanionMods.contains(cMod)) { newCompanionMods.add(cMod); } companionModFacet.add(id, cMod); addHD += cMod.getSafe(IntegerKey.HIT_DIE); } } } Map<String, Integer> varmap = cMod.getMapFor(MapKey.APPLIED_VARIABLE); for (String varName : varmap.keySet()) { final int mLev = mPC.getVariableValue(varName, Constants.EMPTY_STRING).intValue() + aM.getAdjustment(); if (mLev >= cMod.getVariableApplied(varName)) { if (cMod.qualifies(this, cMod)) { if (!oldCompanionMods.contains(cMod)) { newCompanionMods.add(cMod); } companionModFacet.add(id, cMod); addHD += cMod.getSafe(IntegerKey.HIT_DIE); } } } } // Add additional HD if required LevelCommandFactory lcf = getRace().get(ObjectKey.MONSTER_CLASS); final int usedHD = aM.getUsedHD(); addHD -= usedHD; // if ((newClass != null) && (addHD != 0)) if ((lcf != null) && (addHD != 0)) { // set the new HD (but only do it once!) incrementClassLevel(addHD, lcf.getPCClass(), true); aM.setUsedHD(addHD + usedHD); setDirty(true); } // If it's a familiar, we need to change it's Skills if (masterFacet.getUseMasterSkill(id)) { final Collection<Skill> mList = mPC.getSkillSet(); final List<Skill> sKeyList = new ArrayList<>(); // now we have to merge the two lists together and // take the higher rank of each skill for the Familiar for (Skill fSkill : getSkillSet()) { for (Skill mSkill : mList) { // first check to see if familiar // already has ranks in the skill if (mSkill.equals(fSkill)) { // need higher rank of the two Float totalMasterRank = SkillRankControl.getTotalRank(mPC, mSkill); if (totalMasterRank.intValue() > this.getRank(fSkill) .intValue()) { // first zero current SkillRankControl.setZeroRanks(lcf == null ? null : lcf.getPCClass(), this, fSkill); // We don't pass in a class here so that the real // skills can be distinguished from the ones from // the master. SkillRankControl.modRanks(totalMasterRank.doubleValue(), null, true, this, fSkill); } } // build a list of all skills a master // Possesses, but the familiar does not if (!hasSkill(mSkill) && !sKeyList.contains(mSkill)) { sKeyList.add(mSkill); } } } // now add all the skills only the master has for (Skill newSkill : sKeyList) { // familiar doesn't have skill, // but master does, so add it final double sr = SkillRankControl.getTotalRank(mPC, newSkill).doubleValue(); // We don't pass in a class here so that the real skills can be // distinguished from the ones form the master. SkillRankControl.modRanks(sr, null, true, this, newSkill); if (ChooseActivation.hasNewChooseToken(newSkill)) { //TODO a bit reckless :P ChooseInformation<Language> chooseInfo = (ChooseInformation<Language>) newSkill .get(ObjectKey.CHOOSE_INFO); List<? extends Language> selected = chooseInfo.getChoiceActor().getCurrentlySelected( newSkill, mPC); ChoiceManagerList<Language> controller = ChooserUtilities.getConfiguredController(newSkill, this, null, new ArrayList<>()); for (Language lang : selected) { if (!controller.conditionallyApply(this, lang)) { Logging .errorPrint("Failed to add master's language " + lang + " to companion."); } } } } } oldCompanionMods.removeAll(companionModFacet.getSet(id)); for (CompanionMod cMod : oldCompanionMods) { CDOMObjectUtilities.removeAdds(cMod, this); CDOMObjectUtilities.restoreRemovals(cMod, this); } for (CompanionMod cMod : newCompanionMods) { CDOMObjectUtilities.addAdds(cMod, this); CDOMObjectUtilities.checkRemovals(cMod, this); for (CDOMReference<PCTemplate> ref : cMod.getSafeListFor(ListKey.TEMPLATE)) { for (PCTemplate pct : ref.getContainedObjects()) { addTemplate(pct); } } for (CDOMReference<PCTemplate> ref : cMod.getSafeListFor(ListKey.REMOVE_TEMPLATES)) { for (PCTemplate pct : ref.getContainedObjects()) { removeTemplate(pct); } } for (TransitionChoice<Kit> kit : cMod.getSafeListFor(ListKey.KIT_CHOICE)) { kit.act(kit.driveChoice(this), cMod, this); } } calcActiveBonuses(); setDirty(true); } /** * Returns the maximum number of followers this character can have from * the given companion list. This method does not adjust for any followers * already selected by the character. * * @param cList * A list of potential follower races * @return The max number of followers -1 for any number */ public int getMaxFollowers(CompanionList cList) { int ret = followerLimitFacet.getMaxFollowers(id, cList); return (ret == -1) ? getOldFollowerLimit(cList) : ret; } private int getOldFollowerLimit(CompanionList cList) { // Old way of handling this // If the character qualifies for any companion mod of this type // they can take unlimited number of them. for (CompanionMod cMod : Globals.getContext().getReferenceContext().getManufacturer( CompanionMod.class, cList).getAllObjects()) { Map<String, Integer> varmap = cMod.getMapFor(MapKey.APPLIED_VARIABLE); for (String varName : varmap.keySet()) { if (this.getVariableValue(varName, Constants.EMPTY_STRING).intValue() > 0) { return -1; } } Map<CDOMSingleRef<? extends PCClass>, Integer> ac = cMod.getMapFor(MapKey.APPLIED_CLASS); for (Map.Entry<CDOMSingleRef<? extends PCClass>, Integer> me : ac.entrySet()) { PCClass pcclass = me.getKey().get(); String key = pcclass.getKeyName(); for (PCClass pcClass : getClassSet()) { if (pcClass.getKeyName().equals(key)) { return me.getValue(); } } } } return 0; } /** * Get the PlayerCharacter that is the "master" for this object. * * @return master PC */ public PlayerCharacter getMasterPC() { Follower followerMaster = masterFacet.get(id); if (followerMaster == null) { return null; } for (PlayerCharacter nPC : Globals.getPCList()) { if (followerMaster.getFileName().equals(nPC.getFileName())) { return nPC; } } // could not find a filename match, let's try the Name for (PlayerCharacter nPC : Globals.getPCList()) { if (followerMaster.getName().equals(nPC.getName())) { return nPC; } } // no Name and no FileName match, so must not be loaded return null; } /** * Sets the character's name. * * @param aString * A name to set. */ public void setName(final String aString) { setStringFor(PCStringKey.NAME, aString); } /** * Gets the character's name. * * @return The name */ public String getName() { return getSafeStringFor(PCStringKey.NAME); } /** * Takes all the Temporary Bonuses and Merges them into just the unique * named bonuses. * * @return List of Strings */ public List<String> getNamedTempBonusList() { return bonusManager.getNamedTempBonusList(); } /** * Takes all the Temporary Bonuses and Merges them into just the unique * named bonuses. * * @return List of Strings */ public List<String> getNamedTempBonusDescList() { return bonusManager.getNamedTempBonusDescList(); } /** * Set the value of the feat pool. * @param pool value to set the feat pool to */ public void setPoolAmount(final int pool) { poolAmount = pool; } /** * Get the value of the feat pool. * @return the feat pool amount */ public int getPoolAmount() { return poolAmount; } /** * Selector Sets the path to the portrait of the character. * * @param newPortraitPath * the path to the portrait file */ public void setPortraitPath(final String newPortraitPath) { setStringFor(PCStringKey.PORTRAIT_PATH, newPortraitPath); } /** * Set a new outline for the portrait thumbnail. * @param rect The thumbnail outline. */ public void setPortraitThumbnailRect(Rectangle rect) { portraitThumbnailRectFacet.set(id, (Rectangle) rect.clone()); } /** * Get the character's race. * * @return the character's race */ public Race getRace() { return raceFacet.get(id); } /** * Set the character's region. * * @param r the character's region */ public void setRegion(Region r) { regionFacet.setRegion(id, r); } /** * Set the location of the HTML sheet for this character. * * @param aString the location of the HTML sheet */ public void setSelectedCharacterHTMLOutputSheet(final String aString) { outputSheetHTML = aString; } /** * Get the Location of HTML Output Sheet to be used for this character. * * @return HTML output sheet */ public String getSelectedCharacterHTMLOutputSheet() { return outputSheetHTML; } /** * Set the location of the PDF Output Sheet for this character. * * @param aString the location of the PDF Output Sheet */ public void setSelectedCharacterPDFOutputSheet(final String aString) { outputSheetPDF = aString; } /** * Get the location of the PDF Output Sheet to be used for this character. * * @return pdf output sheet */ public String getSelectedCharacterPDFOutputSheet() { return outputSheetPDF; } /** * Get list of shield proficiencies. * * @return shield prof list */ public Collection<ProfProvider<ShieldProf>> getShieldProfList() { return shieldProfFacet.getQualifiedSet(id); } /** * Get skill list. * * @return list of skills */ public Collection<Skill> getSkillSet() { return skillFacet.getSet(id); } /** * Get skill points. * * @return skill points */ public int getSkillPoints() { int returnValue = 0; // First compute gained points, and then remove the already spent ones. // We can't use Remaining points because the level may be removed, and // then we have // to display this as -x on the "Total Skill Points" field for (PCLevelInfo li : getLevelInfo()) { returnValue += li.getSkillPointsGained(this); } for (Skill aSkill : getSkillSet()) { for (PCClass pcc : getSkillRankClasses(aSkill)) { if (pcc != null) { Double curRank = getSkillRankForClass(aSkill, pcc); if (curRank == null) { Logging.errorPrint("Got null on ranks for " + aSkill + " in class " + pcc); curRank = 0.0d; } // Only add the cost for skills associated with a class. // Skill ranks from feats etc are free. final int cost = getSkillCostForClass(aSkill, pcc).getCost(); returnValue -= (int) (cost * curRank); } } } if (Globals.getGameModeHasPointPool()) { returnValue += (int) getRemainingFeatPoints(false); // DO NOT CALL // getFeats() here! It // will set up a // recursive loop and // result in a stack // overflow! } return returnValue; } /** * Get list of special abilities. * * @return List of special abilities */ public List<SpecialAbility> getSpecialAbilityList() { // aList will contain a list of SpecialAbility objects List<SpecialAbility> aList = new ArrayList<>(); aList.addAll(userSpecialAbilityFacet.getAllResolved(id, SA_PROC)); aList.addAll(specialAbilityFacet.getAllResolved(id, SA_PROC)); Collections.sort(aList); return aList; } /** * Get list of special abilities as Strings. * * @return List of special abilities as Strings */ private List<String> getSpecialAbilityListStrings() { List<String> bList = new ArrayList<>(); bList.addAll(userSpecialAbilityFacet.getAllResolved(id, SA_TO_STRING_PROC)); bList.addAll(specialAbilityFacet.getAllResolved(id, SA_TO_STRING_PROC)); Collections.sort(bList); return bList; } /** * same as getSpecialAbilityList except if if you have the same ability * twice, it only lists it once with (2) at the end. * * @return List */ public ArrayList<String> getSpecialAbilityTimesList() { final List<String> abilityList = getSpecialAbilityListStrings(); final List<String> sortList = new ArrayList<>(); final int[] numTimes = new int[abilityList.size()]; for (int i = 0; i < abilityList.size(); i++) { final String ability = abilityList.get(i); if (sortList.contains(ability)) { for (int j = 0; j < sortList.size(); j++) { final String testAbility = sortList.get(j); if (testAbility.equals(ability)) { numTimes[j]++; } } } else { sortList.add(ability); numTimes[i] = 1; } } final ArrayList<String> retList = new ArrayList<>(); for (int i = 0; i < sortList.size(); i++) { String ability = sortList.get(i); if (numTimes[i] > 1) { ability = ability + " (" + numTimes[i] + ")"; } retList.add(ability); } return retList; } /** * Set the name of the spellbook to auto add new known spells to. * * @param aString * The new spellbook name. */ public void setSpellBookNameToAutoAddKnown(final String aString) { setStringFor(PCStringKey.SPELLBOOK_AUTO_ADD_KNOWN, aString); } /** * Get the name of the spellbook to auto add new known spells to. * * @return spellbook name */ public String getSpellBookNameToAutoAddKnown() { return getSafeStringFor(PCStringKey.SPELLBOOK_AUTO_ADD_KNOWN); } /** * Retrieve a spell book object given the name of the spell book. * * @param name * The name of the spell book to be retrieved. * @return The spellbook (or null if not present). */ public SpellBook getSpellBookByName(final String name) { return spellBookFacet.getBookNamed(id, name); } /** * Get spell books. * * @return spellBooks */ private List<String> getSpellBookNames() { return new ArrayList<>(spellBookFacet.getBookNames(id)); } /** * Get spell class given an index. * * @param ix the index * @return spell class */ public PObject getSpellClassAtIndex(final int ix) { final List<? extends PObject> aList = getSpellClassList(); if ((ix >= 0) && (ix < aList.size())) { return aList.get(ix); } return null; } /** * Get spell level temp. * * @return temp spell level */ public int getSpellLevelTemp() { return spellLevelTemp; } /** * Set whether the field should be hidden from output. * @param field The BiographyField to set export suppression rules for. * @param suppress Should the field be hidden from output. */ public void setSuppressBioField(BiographyField field, boolean suppress) { if (suppressBioFieldFacet.setSuppressField(id, field, suppress)) { setDirty(true); } } /** * Temp Bonus list. * * @return List */ public Map<BonusObj, BonusManager.TempBonusInfo> getTempBonusMap() { return bonusManager.getTempBonusMap(); } /** * Get temp bonus filters. * * @return temp bonus filters */ public Set<String> getTempBonusFilters() { return bonusManager.getTempBonusFilters(); } /** * Add the temp bonus to the filter, thus removing its effects from the character. * * @param aBonusStr the temporary bonus to add. */ public void setTempBonusFilter(final String aBonusStr) { bonusManager.addTempBonusFilter(aBonusStr); calcActiveBonuses(); } /** * Remove the temp bonus from the filter, thus restoring its effects to the character. * * @param aBonusStr the temporary bonus to remove. */ public void unsetTempBonusFilter(final String aBonusStr) { bonusManager.removeTempBonusFilter(aBonusStr); calcActiveBonuses(); } /** * Get a set of the templates applies to this pc. * @return the set of Templates. */ public Collection<PCTemplate> getTemplateSet() { return templateFacet.getSet(id); } /** * Evaluates the variable string passed in and returns its value. * * This should probably be refactored to return a String instead. * * @param variableString the variable to evaluate * @param isMax if multiple values are stored, whether to return the largest value * found or the first. * @return the value of the variable. */ public Float getVariable(final String variableString, final boolean isMax) { double value = 0.0; boolean found = false; if (lastVariable != null) { if (lastVariable.equals(variableString)) { if (Logging.isDebugMode()) { final String sb = "This is a deliberate warning message, not an error - " + "Avoiding infinite loop in getVariable: repeated lookup " + "of \"" + lastVariable + "\" at " + value; Logging.debugPrint(sb); } lastVariable = null; return new Float(value); } } try { VariableKey vk = VariableKey.valueOf(variableString); Double val = variableFacet.getVariableValue(id, vk, isMax); if (val != null) { value = val; found = true; } } catch (IllegalArgumentException e) { //This variable is not in the data - must be builtin? } boolean includeBonus = true; if (!found) { lastVariable = variableString; value = getVariableValue(variableString, Constants.EMPTY_STRING); includeBonus = false; found = true; lastVariable = null; } if (found && includeBonus) { value += getTotalBonusTo("VAR", variableString); } return new Float(value); } public void setPointBuyPoints(final int argPointBuyPoints) { pointBuyPoints = argPointBuyPoints; } public int getPointBuyPoints() { return pointBuyPoints; } public void setXP(final int xp) { if (xpFacet.setXP(id, xp)) { setDirty(true); } } public int getXP() { return xpFacet.getXP(id); } public void setXPTable(final String xpTableName) { if (xpTableFacet.set(id, SettingsHandler.getGame().getLevelInfo(xpTableName))) { setDirty(true); } } public LevelInfo getXPTableLevelInfo(int level) { return xpTableFacet.getLevelInfo(id, level); } public void setCharacterType(final String characterType) { if (characterTypeFacet.set(id, characterType)) { setDirty(true); } } public void setPreviewSheet(final String previewSheet) { if (previewSheetFacet.set(id, previewSheet)) { setDirty(true); } } public void addEquipSet(final EquipSet set) { equipSetFacet.add(id, set); //setDirty(true); } /** * Add an item of equipment to the character. * * @param eq * The equipment to be added. */ public void addEquipment(final Equipment eq) { equipmentFacet.add(id, eq, this); userEquipmentFacet.add(id, eq, this); //setDirty(true); } /** * Cache the output index of an automatic equipment item. * @param item The equipment item. */ public void cacheOutputIndex(Equipment item) { if (item.isAutomatic()) { if (Logging.isDebugMode()) { Logging.debugPrint("Caching " + item.getKeyName() + " - " + item.getOutputIndex() + " item"); } autoEquipOutputOrderCache.put(item.getKeyName(), item.getOutputIndex()); } } /** * Update the number of a particular equipment item the character possesses. * Mostly concerned with ensuring that the spellbook objects remain in sync * with the number of equipment spellbooks. * * @param eq * The Equipment being updated. * @param oldQty * The original number of items. * @param newQty * The new number of items. */ public void updateEquipmentQty(final Equipment eq, double oldQty, double newQty) { if (eq.isType(Constants.TYPE_SPELLBOOK)) { String baseBookname = eq.getName(); String bookName = eq.getName(); int old = (int) oldQty; int newQ = (int) newQty; // Add any new items for (int i = old; i < newQ; i++) { if (i > 0) { bookName = baseBookname + " #" + (i + 1); } SpellBook book = spellBookFacet.getBookNamed(id, bookName); if (book == null) { book = new SpellBook(bookName, SpellBook.TYPE_SPELL_BOOK); } book.setEquip(eq); addSpellBook(book); } // Remove any old items for (int i = old; i > newQ; i--) { if (i > 0) { bookName = baseBookname + " #" + i; } delSpellBook(bookName); } } setDirty(true); } public void addFollower(final Follower aFollower) { if (followerFacet.add(id, aFollower)) { setDirty(true); } } private void addLocalEquipment(final Equipment eq) { equipmentFacet.add(id, eq, this); } public void addNotesItem(final NoteItem item) { if (noteItemFacet.add(id, item)) { setDirty(true); } } /** * Adds a "temporary" bonus. * * @param aBonus The bonus object to add. * @param source The source of the temporary bonus * @param target The object getting the bonus (typically the PC, can also be equipment). * @return The bonus info representing the added instance of the bonus. */ public TempBonusInfo addTempBonus(final BonusObj aBonus, Object source, Object target) { TempBonusInfo tempBonusInfo = bonusManager.addTempBonus(aBonus, source, target); setDirty(true); return tempBonusInfo; } /** * Add a piece of equipment to the temporary bonus list. * @param aEq The piece of equipment to add. */ public void addTempBonusItemList(final Equipment aEq) { tempBonusItemList.add(aEq); setDirty(true); } /** * Compute the total bonus from a List of BonusObjs. * * @param aList The list of objects * @param source The source of the bonus objects. * @return The aggregate bonus */ public double calcBonusFromList(final List<BonusObj> aList, CDOMObject source) { double iBonus = 0; for (BonusObj bonus : aList) { iBonus += bonus.resolve(this, source.getQualifiedKey()).doubleValue(); } return iBonus; } /** * Checks that the parameter passed in is in the list of objects for which this PC qualifies. * @param obj the object to test for qualification. * @return true if the PC is qualified to have this object. */ public boolean checkQualifyList(CDOMObject obj) { return qualifyFacet.grantsQualify(id, obj); } /** * Check whether this PC has this WeaponProf. * @param wp The WeaponProf to check. * @return True if the PC has the WeaponProf */ public boolean hasWeaponProf(final WeaponProf wp) { return weaponProfFacet.containsProf(id, wp); } /** * Remove an EqSet from the PC's Equipped Equipment. * @param eSet - The EquipSet to remove. * @return true if the object was removed. */ public boolean delEquipSet(final EquipSet eSet) { boolean found = equipSetFacet.delEquipSet(id, eSet); setDirty(true); return found; } /** * Remove a Follower from this PC. * @param aFollower The follower to remove. */ public void delFollower(final Follower aFollower) { followerFacet.remove(id, aFollower); setDirty(true); } /** * Check whether the PC has this variable. * @param variableString The variable to check for. * @return True if the PC has the variable. */ public boolean hasVariable(final String variableString) { try { return variableFacet.contains(id, VariableKey.valueOf(variableString)); } catch (IllegalArgumentException e) { //Built in variable return false; } } /** * * @param eq */ public void removeEquipment(final Equipment eq) { if (eq.isType(Constants.TYPE_SPELLBOOK)) { delSpellBook(eq.getName()); } equipmentFacet.remove(id, eq, this); userEquipmentFacet.remove(id, eq, this); setDirty(true); } /** * * @param eq */ private void removeLocalEquipment(final Equipment eq) { equipmentFacet.remove(id, eq, this); setDirty(true); } public void setAlignment(PCAlignment align) { if (alignmentFacet.set(id, align)) { setDirty(true); } } /** * @param allowDebt the allowDebt to set */ public void setAllowDebt(boolean allowDebt) { allowDebtFacet.set(id, allowDebt); } public String getAttackString(AttackType at) { return getAttackString(at, 0); } public String getAttackString(AttackType at, final int bonus) { return getAttackString(at, bonus, 0); } /** * Calculates and returns an attack string for one of Melee, Ranged or * Unarmed damage. This will be returned in attack string format i.e. * +11/+6/+1. The attack string returned by this function normally only * includes the attacks generated by the characters Base Attack Bonus. There * are two bonuses to TOHIT that may be applied to the attack string * returned by this function. The first bonus increases only the size of the * attacks generated. The second increases both the size and number of * attacks * * @param at * The type of attack. Takes an AttackType (an enumeration) * * @param TOHITBonus * A bonus that will be added to the TOHIT numbers. This bonus * affects only the numbers produced, not the number of attacks * * @param BABBonus * This bonus will be added to BAB before the number of attacks * has been determined. * @return The attack string for this character */ public String getAttackString(AttackType at, final int TOHITBonus, int BABBonus) { final String cacheLookup = "AttackString:" + at.getIdentifier() + "," + TOHITBonus + "," + BABBonus; final String cached = variableProcessor.getCachedString(cacheLookup); if (cached != null) { return cached; } // index: 0 = melee; 1 = ranged; 2 = unarmed // now we see if this PC is a Familiar // Initialise to some large negative number int masterBAB = -9999; int masterTotal = -9999; final PlayerCharacter nPC = getMasterPC(); if ((nPC != null) && (!masterFacet.getCopyMasterBAB(id).isEmpty())) { masterBAB = nPC.baseAttackBonus(); final String copyMasterBAB = replaceMasterString(masterFacet.getCopyMasterBAB(id), masterBAB); masterBAB = getVariableValue(copyMasterBAB, Constants.EMPTY_STRING).intValue(); masterTotal = masterBAB + TOHITBonus; } final int BAB = baseAttackBonus(); int attackCycle = 1; int workingBAB = BAB + TOHITBonus; int subTotal = BAB; int raceBAB = 0; final List<Integer> ab = new ArrayList<>(10); final StringBuilder attackString = new StringBuilder(30); // Assume a max of 10 attack cycles for (int total = 0; total < 10; ++total) { ab.add(0); } // Some classes (like the Monk or Ranged Sniper) use // a different attack cycle than the standard classes // So compute the base attack for this type (BAB, RAB, UAB) for (PCClass pcClass : getClassSet()) { // Get the attack bonus final int b = pcClass.baseAttackBonus(this); // Get the attack cycle final int c = pcClass.attackCycle(at); // add to all other classes if (c < ab.size()) { final int d = ab.get(c).intValue() + b; // set new value for iteration ab.set(c, d); } if (c != 3) { raceBAB += b; } } // Iterate through all the possible attack cycle values // and find the one with the highest attack value for (int i = 2; i < 10; ++i) { final int newAttack = ab.get(i).intValue(); final int oldAttack = ab.get(attackCycle).intValue(); if ((newAttack / i) > (oldAttack / attackCycle)) { attackCycle = i; } } // total Number of Attacks for this PC int attackTotal = ab.get(attackCycle).intValue(); // Default cut-off before multiple attacks (e.g. 5) final int defaultAttackCycle = SettingsHandler.getGame().getBabAttCyc(); if (attackTotal == 0) { attackCycle = defaultAttackCycle; } // FAMILIAR: check to see if the masters BAB is better workingBAB = Math.max(workingBAB, masterTotal); subTotal = Math.max(subTotal, masterBAB); raceBAB = Math.max(raceBAB, masterBAB); if (attackCycle != defaultAttackCycle) { if ((attackTotal / attackCycle) < (subTotal / defaultAttackCycle)) { attackCycle = defaultAttackCycle; attackTotal = subTotal; } else { workingBAB -= raceBAB; subTotal -= raceBAB; } } int maxAttacks = SettingsHandler.getGame().getBabMaxAtt(); final int minMultiBab = SettingsHandler.getGame().getBabMinVal(); // If there is a bonus to BAB, it needs to be added to ALL of // the variables used to determine the number of attacks attackTotal += BABBonus; workingBAB += BABBonus; subTotal += BABBonus; do { if (attackString.length() > 0) { attackString.append('/'); } attackString.append(Delta.toString(workingBAB)); workingBAB -= attackCycle; attackTotal -= attackCycle; subTotal -= attackCycle; maxAttacks--; } while (((attackTotal >= minMultiBab) || (subTotal >= minMultiBab)) && (maxAttacks > 0)); variableProcessor.addCachedString(cacheLookup, attackString.toString()); return attackString.toString(); } /** * @return the autoResize */ public boolean isAutoResize() { return autoResize; } /** * @param autoResize the autoResize to set */ public void setAutoResize(boolean autoResize) { this.autoResize = autoResize; } /** * Sets the autoSortGear. * * @param autoSortGear * The autoSortGear to set */ public void setAutoSortGear(final boolean autoSortGear) { if (this.autoSortGear != autoSortGear) { this.autoSortGear = autoSortGear; setDirty(true); } } /** * Returns the autoSortGear. * * @return boolean */ public boolean isAutoSortGear() { return autoSortGear; } /** * whether we should add auto known spells at level up * * @param aBool */ public void setAutoSpells(final boolean aBool) { if (autoKnownSpells != aBool) { autoKnownSpells = aBool; setDirty(true); } } public boolean getAutoSpells() { return autoKnownSpells; } /** * @param ignoreCost the ignoreCost to set */ public void setIgnoreCost(boolean ignoreCost) { ignoreCostFacet.set(id, ignoreCost); } /** * Determine whether higher level known spell slots can be used for lower * level spells, or if known spells are restricted to their own level only. * * @return Returns the useHigherKnownSlots. */ public boolean getUseHigherKnownSlots() { return useHigherKnownSlots; } /** * Set whether higher level known spell slots can be used for lower level * spells, or if known spells are restricted to their own level only. * * @param useHigher * Can higher level known spell slots be used? */ public void setUseHigherKnownSlots(boolean useHigher) { this.useHigherKnownSlots = useHigher; } /** * Determine whether higher level prepared spell slots can be used for lower * level spells, or if prepared spells are restricted to their own level * only. * * @return Returns the useHigherPreppedSlots. */ public boolean getUseHigherPreppedSlots() { return useHigherPreppedSlots; } /** * Set whether higher level prepared spell slots can be used for lower level * spells, or if prepared spells are restricted to their own level only. * * @param useHigher * Can higher level prepared spell slots be used? */ public void setUseHigherPreppedSlots(boolean useHigher) { this.useHigherPreppedSlots = useHigher; } /** * Returns the "Base" check value for the check at the index * specified. * * <p> * This method caps the base check based on the game mode setting for * {@link pcgen.core.GameMode#getChecksMaxLvl() checks max level}. * * @param check * The index of the check to get * * @return The base check value. */ public int getBaseCheck(final PCCheck check) { String checkVar = ControlUtilities .getControlToken(Globals.getContext(), CControl.BASESAVE); if (checkVar != null) { return ((Number) this.getLocal(check, checkVar)).intValue(); } final String cacheLookup = "getBaseCheck:" + check.getKeyName(); //$NON-NLS-1$ Float total = variableProcessor.getCachedVariable(cacheLookup); if (total != null) { return total.intValue(); } double bonus = 0; final String checkName = check.getKeyName(); //Apply non-magical bonuses bonus += getTotalBonusTo("SAVE", "BASE." + checkName); // // now we see if this PC is a Familiar/Mount final PlayerCharacter nPC = getMasterPC(); if ((nPC != null) && (!masterFacet.getCopyMasterCheck(id).isEmpty())) { int masterBonus = nPC.getBaseCheck(check); final String copyMasterCheck = replaceMasterString(masterFacet.getCopyMasterCheck(id), masterBonus); masterBonus = getVariableValue(copyMasterCheck, Constants.EMPTY_STRING).intValue(); // use masters save if better bonus = Math.max(bonus, masterBonus); } variableProcessor.addCachedVariable(cacheLookup, (float) bonus); return (int) bonus; } /** * Returns the total check value for the check specified for the character. * * <p> * This total includes all check bonuses the character has. * * @param check * The check to get. * * @return A check value. */ public int getTotalCheck(PCCheck check) { String checkVar = ControlUtilities .getControlToken(Globals.getContext(), CControl.TOTALSAVE); if (checkVar != null) { return ((Number) this.getLocal(check, checkVar)).intValue(); } return getBaseCheck(check) + (int) getTotalBonusTo("SAVE", check.getKeyName()); } /** * return bonus total for a specific bonusType e.g: * getBonusDueToType("COMBAT","AC","Armor") to get armor bonuses * * @param mainType * @param subType * @param bonusType * @return bonus due to type */ public double getBonusDueToType(final String mainType, final String subType, final String bonusType) { return bonusManager.getBonusDueToType(mainType, subType, bonusType); } public void setDeity(final Deity aDeity) { if (canSelectDeity(aDeity) && deityFacet.set(id, aDeity)) { setDirty(true); } } /** * Retrieves an unsorted list of the character's equipment matching the * supplied type and status criteria. * * @param typeName * The type of equipment to be selected * @param status * The required status: 1 (equipped) 2 (not equipped) 3 (don't * care) * @return An ArrayList of the matching equipment objects. */ public List<Equipment> getEquipmentOfType(final String typeName, final int status) { return getEquipmentOfType(typeName, Constants.EMPTY_STRING, status); } /** * Retrieves an unsorted list of the character's equipment matching the * supplied type, sub type and status criteria. * * @param typeName * The type of equipment to be selected * @param subtypeName * The subtype of equipment to be selected (empty string for no * subtype) * @param status * The required status: 1 (equipped) 2 (not equipped) 3 (don't * care) * @return An ArrayList of the matching equipment objects. */ public List<Equipment> getEquipmentOfType(final String typeName, final String subtypeName, final int status) { final List<Equipment> aArrayList = new ArrayList<>(); for (Equipment eq : getEquipmentSet()) { final boolean subTypeOk = Constants.EMPTY_STRING.equals(subtypeName) || eq.typeStringContains(subtypeName); final boolean statusOk = status == 3 || (status == 2 && !eq.isEquipped()) || (status == 1 && eq.isEquipped()); if (eq.typeStringContains(typeName) && subTypeOk && statusOk) { aArrayList.add(eq); } } return aArrayList; } /** * Retrieves a list, sorted in output order, of the character's equipment * matching the supplied type and status criteria. This list is in ascending * order of the equipment's outputIndex field. If multiple items of * equipment have the same outputIndex they will be ordered by name. Note * hidden items (outputIndex = -1) are not included in this list. * * @param typeName * The type of equipment to be selected * @param status * The required status: 1 (equipped) 2 (not equipped) 3 (don't * care) * @return An ArrayList of the matching equipment objects in output order. */ public List<Equipment> getEquipmentOfTypeInOutputOrder(final String typeName, final int status) { return sortEquipmentList(getEquipmentOfType(typeName, status), Constants.MERGE_ALL); } /** * @param typeName * The type of equipment to be selected * @param status * The required status * @param merge * What type of merge for like equipment * @return An ArrayList of equipment objects */ public List<Equipment> getEquipmentOfTypeInOutputOrder(final String typeName, final int status, final int merge) { return sortEquipmentList(getEquipmentOfType(typeName, status), merge); } /** * @param typeName * The type of equipment to be selected * @param subtypeName * The subtype of equipment to be selected * @param status * The required status * @param merge * What sort of merging should occur * @return An ArrayList of equipment objects */ public List<Equipment> getEquipmentOfTypeInOutputOrder(final String typeName, final String subtypeName, final int status, final int merge) { return sortEquipmentList(getEquipmentOfType(typeName, subtypeName, status), merge); } /** * Retrieve the expanded list of weapons Expanded weapons include: double * weapons and melee+ranged weapons Output order is assumed Merge of like * equipment depends on the passed in int * * @param merge The type of merge to perform * * @return the sorted list of weapons. */ public List<Equipment> getExpandedWeapons(final int merge) { final List<Equipment> weapList = sortEquipmentList(getEquipmentOfType("Weapon", 3), merge); // // If any weapon is both Melee and Ranged, then make 2 weapons // for list, one Melee only, the other Ranged and Thrown. // For double weapons, if wielded in two hands show attacks // for both heads, head 1 and head 2 else // if wielded in 1 hand, just show damage by head // for (int idx = 0; idx < weapList.size(); ++idx) { final Equipment equip = weapList.get(idx); if (equip.isDouble() && (equip.getLocation() == EquipmentLocation.EQUIPPED_TWO_HANDS)) { Equipment eqm = equip.clone(); eqm.removeType(Type.DOUBLE); eqm.addType(Type.HEAD1); // Add "Head 1 only" to the name of the weapon eqm.setWholeItemName(eqm.getName()); eqm.setName(EquipmentUtilities.appendToName(eqm.getName(), "Head 1 only")); if (!eqm.getOutputName() .contains("Head 1 only")) { eqm.put(StringKey.OUTPUT_NAME, EquipmentUtilities.appendToName(eqm.getOutputName(), "Head 1 only")); } setProf(equip, eqm); weapList.add(idx + 1, eqm); eqm = equip.clone(); final String altType = eqm.getType(false); if (!altType.isEmpty()) { eqm.removeListFor(ListKey.TYPE); for (String s : altType.split("\\.")) { eqm.addType(Type.getConstant(s)); } } eqm.removeType(Type.DOUBLE); eqm.addType(Type.HEAD2); EquipmentHead head = eqm.getEquipmentHead(1); String altDamage = eqm.getAltDamage(this); if (!altDamage.isEmpty()) { head.put(StringKey.DAMAGE, altDamage); } head.put(IntegerKey.CRIT_MULT, eqm.getAltCritMultiplier()); head.put(IntegerKey.CRIT_RANGE, eqm.getRawCritRange(false)); head.removeListFor(ListKey.EQMOD); head.addAllToListFor(ListKey.EQMOD, eqm.getEqModifierList(false)); // Add "Head 2 only" to the name of the weapon eqm.setWholeItemName(eqm.getName()); eqm.setName(EquipmentUtilities.appendToName(eqm.getName(), "Head 2 only")); if (!eqm.getOutputName() .contains("Head 2 only")) { eqm.put(StringKey.OUTPUT_NAME, EquipmentUtilities.appendToName(eqm.getOutputName(), "Head 2 only")); } setProf(equip, eqm); weapList.add(idx + 2, eqm); } // // Leave else here, as otherwise will show attacks // for both heads for thrown double weapons when // it should only show one // else if (equip.isMelee() && equip.isRanged()) { // // Strip off the Ranged portion, set range to 0 // Equipment eqm = equip.clone(); eqm.addType(Type.BOTH); eqm.removeType(Type.RANGED); eqm.removeType(Type.THROWN); eqm.put(IntegerKey.RANGE, 0); setProf(equip, eqm); weapList.set(idx, eqm); boolean replacedPrimary = primaryWeaponFacet.replace(id, equip, eqm); boolean replacedSecondary = secondaryWeaponFacet.replace(id, equip, eqm); // // Add thrown portion, strip Melee // Equipment eqr = equip.clone(); eqr.addType(Type.RANGED); eqr.addType(Type.THROWN); eqr.addType(Type.BOTH); eqr.removeType(Type.MELEE); // Add "Thrown" to the name of the weapon eqr.setName(EquipmentUtilities.appendToName(eqr.getName(), "Thrown")); if (!eqr.getOutputName() .contains("Thrown")) { eqr.put(StringKey.OUTPUT_NAME, EquipmentUtilities.appendToName(eqr.getOutputName(), "Thrown")); } setProf(equip, eqr); weapList.add(++idx, eqr); if (replacedPrimary) { primaryWeaponFacet.addAfter(id, eqm, eqr); } else if (replacedSecondary) { secondaryWeaponFacet.addAfter(id, eqm, eqr); } } } return weapList; } /** * Calculates total bonus from Feats * * @param aType * @param aName * @return feat bonus to */ public double getFeatBonusTo(String aType, String aName) { final Map<String, Ability> aHashMap = new HashMap<>(); for (Ability aFeat : getAbilityList(AbilityCategory.FEAT, Nature.NORMAL)) { if (aFeat != null) { aHashMap.put(aFeat.getKeyName(), aFeat); } } addUniqueAbilitiesToMap(aHashMap, getAbilityList(AbilityCategory.FEAT, Nature.VIRTUAL)); List<Ability> aggregateFeatList = new ArrayList<>(); addUniqueAbilitiesToMap(aHashMap, getAbilityList(AbilityCategory.FEAT, Nature.AUTOMATIC)); aggregateFeatList.addAll(aHashMap.values()); return getPObjectWithCostBonusTo(aggregateFeatList, aType.toUpperCase(), aName.toUpperCase()); } public Ability getMatchingAbility(Category<Ability> abilityCategory, Ability ability, Nature nature) { Collection<CNAbility> cnas = grantedAbilityFacet.getPoolAbilities(id, abilityCategory, nature); for (CNAbility cna : cnas) { if (cna.getAbilityKey().equals(ability.getKeyName())) { return cna.getAbility(); } } return null; } public void setHasMadeKitSelectionForAgeSet(final int index, final boolean arg) { if ((index >= 0) && (index < 10)) { ageSetKitSelections[index] = arg; } setDirty(true); } public Collection<Kit> getKitInfo() { return kitFacet.getSet(id); } public Collection<PCLevelInfo> getLevelInfo() { return levelInfoFacet.getSet(id); } public Collection<PCLevelInfo> clearLevelInfo() { return levelInfoFacet.removeAll(id); } public PCLevelInfo getLevelInfo(int index) { return levelInfoFacet.get(id, index); } public String getLevelInfoClassKeyName(final int idx) { if ((idx >= 0) && (idx < getLevelInfoSize())) { return levelInfoFacet.get(id, idx).getClassKeyName(); } return Constants.EMPTY_STRING; } public PCLevelInfo getLevelInfoFor(final String classKey, int level) { for (PCLevelInfo pcl : getLevelInfo()) { if (pcl.getClassKeyName().equals(classKey)) { level--; } if (level <= 0) { return pcl; } } return null; } public int getLevelInfoSize() { return levelInfoFacet.getCount(id); } /** * whether we should load companions on master load * * @param aBool */ public void setLoadCompanion(final boolean aBool) { if (autoLoadCompanion != aBool) { autoLoadCompanion = aBool; setDirty(true); } } public boolean getLoadCompanion() { return autoLoadCompanion; } /** * @return the number of Character Domains possible */ public int getMaxCharacterDomains() { return (int) getTotalBonusTo("DOMAIN", "NUMBER"); } /** * @param source * @param aPC * @return the number of Character Domains possible and check the level of * the source class if the result is 0. */ public int getMaxCharacterDomains(final PCClass source, final PlayerCharacter aPC) { int i = getMaxCharacterDomains(); if (i == 0 && !hasDefaultDomainSource()) { i = (int) source.getBonusTo("DOMAIN", "NUMBER", getLevel(source), aPC); } return i; } /** * Calculate the maximum number of ranks the character is allowed to have in * the specified skill. * * @param aSkill * The skill being checked. * @param aClass * The name of the current class in which points are being spent - * only used to check cross-class skill cost. * @return max rank */ public Float getMaxRank(Skill aSkill, final PCClass aClass) { int levelForSkillPurposes = getTotalLevels(); final BigDecimal maxRanks; if (aSkill == null) { return 0.0f; } if (aSkill.getSafe(ObjectKey.EXCLUSIVE)) { // Exclusive skills only count levels in classes which give access // to the skill levelForSkillPurposes = 0; for (PCClass bClass : getClassSet()) { if (this.isClassSkill(bClass, aSkill)) { levelForSkillPurposes += getLevel(bClass); } } if (levelForSkillPurposes == 0) { // No classes qualify for this exclusive skill, so treat it as a // cross-class skill // This does not seem right to me! JD levelForSkillPurposes = (getTotalLevels()); maxRanks = SkillUtilities.maxCrossClassSkillForLevel(levelForSkillPurposes, this); } else { maxRanks = SkillUtilities.maxClassSkillForLevel(levelForSkillPurposes, this); } } else if (!this.isClassSkill(aSkill) && (this.getSkillCostForClass(aSkill, aClass) == SkillCost.CLASS)) { // Cross class skill - but as cost is 1 only return a whole number maxRanks = new BigDecimal(SkillUtilities.maxCrossClassSkillForLevel(levelForSkillPurposes, this).intValue()); // This was (int) (i/2.0) previously } else if (!this.isClassSkill(aSkill)) { // Cross class skill maxRanks = SkillUtilities.maxCrossClassSkillForLevel(levelForSkillPurposes, this); } else { // Class skill maxRanks = SkillUtilities.maxClassSkillForLevel(levelForSkillPurposes, this); } return new Float(maxRanks.floatValue()); } /** * Checks if the stat is a non ability. * * @return true, if is non ability */ public boolean isNonAbility(PCStat stat) { return nonAbilityFacet.isNonAbility(id, stat); } public int getOffHandLightBonus() { final int div = getVariableValue("OFFHANDLIGHTBONUS", Constants.EMPTY_STRING).intValue(); return div; } public boolean isProficientWith(final Equipment eq) { if (eq.isShield()) { return shieldProfFacet.isProficientWithShield(id, eq); } else if (eq.isArmor()) { return armorProfFacet.isProficientWithArmor(id, eq); } else if (eq.isWeapon()) { return weaponProfFacet.isProficientWithWeapon(id, eq); } return false; } /** * Changes the race of the character. First it removes the current Race, * then add the new Race. * * @param newRace */ public boolean setRace(final Race newRace) { boolean success; if (newRace == null) { success = raceInputFacet.set(id, Globals.s_EMPTYRACE); } else { success = raceInputFacet.set(id, newRace); } if (success) { calcActiveBonuses(); } return success; } /** * return bonus from a Race * * @param aType * @param aName * @return race bonus to */ public double getRaceBonusTo(String aType, String aName) { if (getRace() == null) { return 0; } final List<BonusObj> tempList = BonusUtilities.getBonusFromList(getRace().getBonusList(this), aType.toUpperCase(), aName.toUpperCase()); return calcBonusFromList(tempList, getRace()); } public int getSR() { return calcSR(true); } /** * Calculates total bonus from Size adjustments * * @param aType * @param aName * @return size adjustment bonus to */ public double getSizeAdjustmentBonusTo(String aType, String aName) { return getBonusDueToType(aType.toUpperCase(), aName.toUpperCase(), "SIZE"); } /** * Set the skill display filter * * @param filter * The new filter */ public void setSkillFilter(final SkillFilter filter) { if (skillFilterFacet.set(id, filter)) { setDirty(true); } } /** * @return The selected skill display filter. */ public SkillFilter getSkillFilter() { SkillFilter filter = skillFilterFacet.get(id); if (filter == null) { filter = SkillFilter.getByValue(PCGenSettings.OPTIONS_CONTEXT.initInt( PCGenSettings.OPTION_SKILL_FILTER, SkillFilter.Usable.getValue())); if (filter == SkillFilter.SkillsTab) { filter = SkillFilter.Usable; } setSkillFilter(filter); } return filter; } /** * Set the order in which skills should be sorted for output. * * @param i * The new output order */ public void setSkillsOutputOrder(final SkillsOutputOrder i) { if (skillsOutputOrder != i) { skillsOutputOrder = i; setDirty(true); } } /** * @return The selected Output Order for skills. */ public SkillsOutputOrder getSkillsOutputOrder() { return skillsOutputOrder; } /** * Method will go through the list of classes that the player character has * and see if they are a spell caster and of the desired caster level. * * @param minLevel * @return boolean */ public boolean isSpellCaster(final int minLevel) { return isSpellCaster(minLevel, false) > 0; } /** * Method will go through the list of classes that the player character has * and see if they are a spell caster and of the total of all of their * spellcasting levels is at least the desired caster level. * * @param minLevel * The desired caster level * @param sumOfLevels * True if all of the character caster levels should be added * together before the comparison. * @return boolean */ public int isSpellCaster(final int minLevel, final boolean sumOfLevels) { return isSpellCaster(null, minLevel, sumOfLevels); } /** * Method will go through the list of classes that the player character has * and see if they are a spell caster of the desired type and of the desired * caster level. * * @param spellType * The type of spellcaster (i.e. "Arcane" or "Divine") * @param minLevel * The desired caster level * @param sumLevels * True if all of the character caster levels should be added * together before the comparison. * @return boolean */ public int isSpellCaster(final String spellType, final int minLevel, final boolean sumLevels) { int classTotal = 0; int runningTotal = 0; for (PCClass pcClass : getClassSet()) { if (spellType == null || spellType.equalsIgnoreCase(pcClass.getSpellType())) { int classLevels = (int) getTotalBonusTo("CASTERLEVEL", pcClass.getKeyName()); if ((classLevels == 0) && (canCastSpellTypeLevel(pcClass.getSpellType(), 0, 1) || canCastSpellTypeLevel( pcClass.getSpellType(), 1, 1))) { // missing CASTERLEVEL hack classLevels = getLevel(pcClass); } classLevels += (int) getTotalBonusTo("PCLEVEL", pcClass.getKeyName()); if (sumLevels) { runningTotal += classLevels; } else { if (classLevels >= minLevel) { classTotal++; } } } } if (sumLevels) { return runningTotal >= minLevel ? 1 : 0; } return classTotal; } public void getSpellList() { // all non-spellcaster spells are added to race // so return if it's null Race race = getRace(); if (race == null) { return; } activeSpellsFacet.process(id); setDirty(true); } /** * Parses a spells range (short, medium or long) into an Integer based on * the spell and spell casters level * * @param aSpell * The spell being output. * @param si * The info about conditions applied to the spell * @return spell range */ public String getSpellRange(final CharacterSpell aSpell, final SpellInfo si) { String aRange = aSpell.getSpell().getListAsString(ListKey.RANGE); String aSpellClass = aSpell.getVariableSource(this); int rangeInFeet = 0; String aString = SettingsHandler.getGame().getSpellRangeFormula(aRange.toUpperCase()); if (aRange.equalsIgnoreCase("CLOSE") && (aString == null)) { aString = "((CASTERLEVEL/2).TRUNC*5)+25"; //$NON-NLS-1$ } else if (aRange.equalsIgnoreCase("MEDIUM") && (aString == null)) { aString = "(CASTERLEVEL*10)+100"; //$NON-NLS-1$ } else if (aRange.equalsIgnoreCase("LONG") && (aString == null)) { aString = "(CASTERLEVEL*40)+400"; //$NON-NLS-1$ } if (aString != null) { List<Ability> metaFeats = null; if (si != null) { metaFeats = si.getFeatList(); } rangeInFeet = getVariableValue(aSpell, aString, aSpellClass).intValue(); if ((metaFeats != null) && !metaFeats.isEmpty()) { for (Ability feat : metaFeats) { rangeInFeet += (int) BonusCalc.charBonusTo(feat, "SPELL", "RANGE", this); final int iMult = (int) BonusCalc.charBonusTo(feat, "SPELL", "RANGEMULT", this); if (iMult > 0) { rangeInFeet *= iMult; } } } aRange += (" (" + Globals.getGameModeUnitSet().displayDistanceInUnitSet(rangeInFeet) + Globals.getGameModeUnitSet().getDistanceUnit() + ")"); } else { aRange = parseSpellString(aSpell, aRange); } return aRange; } /** * Computes the Caster Level for a Class * * @param aClass * @return caster level for class */ public int getCasterLevelForClass(final PCClass aClass) { final Spell sp = new Spell(); final CharacterSpell cs = new CharacterSpell(aClass, sp); final String aSpellClass = "CLASS:" + aClass.getKeyName(); return getVariableValue(cs, "CASTERLEVEL", aSpellClass).intValue(); } /** * Computes the Caster Level for a Class * * @param aSpell * @return caster level for spell */ public int getCasterLevelForSpell(final CharacterSpell aSpell) { return getVariableValue(aSpell, "CASTERLEVEL", aSpell.getVariableSource(this)).intValue(); } /** * Calculates total bonus from all stats * * @param aType * @param aName * @return stat bonus to */ private double getStatBonusTo(String aType, String aName) { return statBonusFacet.getStatBonusTo(id, aType, aName); } /** * Parses through all templates to calculate total bonus * * @param aType * @param aName * @return template bonus to */ public double getTemplateBonusTo(String aType, String aName) { return getPObjectWithCostBonusTo(templateFacet.getSet(id), aType.toUpperCase(), aName.toUpperCase()); } /** * Get the total bonus from Stats, Size, Age, Alignment, Classes, * companions, Equipment, Feats, Templates, Domains, Races, etc This value * is taken from an already populated HashMap for speed * * @param bonusType * Type of bonus ("COMBAT" or "SKILL") * @param bonusName * Name of bonus ("AC" or "Hide"); * @return total bonus to */ public double getTotalBonusTo(final String bonusType, final String bonusName) { return bonusManager.getTotalBonusTo(bonusType, bonusName); } public int getTotalLevels() { return levelFacet.getTotalLevels(id); } /** * Get the value of the desired stat at the point just before the character * was raised to the next level. * * @param stat * The Stat to check. * @param level * The level we want to see the stat at. * @param includePost * Should stat mods that occurred after levelling be included? * @return The stat as it was at the level */ public int getTotalStatAtLevel(final PCStat stat, final int level, final boolean includePost) { int curStat = this.getTotalStatFor(stat); for (int idx = getLevelInfoSize() - 1; idx >= level; --idx) { final int statLvlAdjust = levelInfoFacet.get(id, idx).getTotalStatMod(stat, true); curStat -= statLvlAdjust; } // If the user doesn't want POST changes, we remove any made in the // target level only if (!includePost && level > 0) { int statLvlAdjust = levelInfoFacet.get(id, level - 1).getTotalStatMod(stat, true); statLvlAdjust -= levelInfoFacet.get(id, level - 1).getTotalStatMod(stat, false); curStat -= statLvlAdjust; } return curStat; } /** * whether we should use/save Temporary bonuses * * @param aBool */ public void setUseTempMods(final boolean aBool) { useTempMods = aBool; // commented out setDirty because this causes a re-load of all tabs // every time any tab is viewed! merton_monk // setDirty(true); } public boolean getUseTempMods() { return useTempMods; } /** * Evaluates a variable for this character e.g: * getVariableValue("3+CHA","CLASS:Cleric") for Turn Undead * * @param aString * The variable to be evaluated * @param src * The source within which the variable is evaluated * @return The value of the variable */ public Float getVariableValue(final String aString, final String src) { return getVariableValue(null, aString, src); } @Override public Float getVariableValue(final String varName, final String src, final PlayerCharacter aPC) { return getVariableValue(null, varName, src); } /** * Evaluates a variable for this character e.g: * getVariableValue("3+CHA","CLASS:Cleric") for Turn Undead * * @param aSpell * This is specifically to compute bonuses to CASTERLEVEL for a * specific spell. * @param aString * The variable to be evaluated * @param src * The source within which the variable is evaluated * @return The value of the variable */ private Float getVariableValue(final CharacterSpell aSpell, String aString, String src) { VariableProcessor vp = variableProcessor; return vp.getVariableValue(aSpell, aString, src, spellLevelTemp); } /** * @return VariableProcessor */ public VariableProcessor getVariableProcessor() { return variableProcessor; } public int getTotalCasterLevelWithSpellBonus(CharacterSpell acs, final Spell aSpell, final String spellType, final String classOrRace, final int casterLev) { if (aSpell != null && acs.getFixedCasterLevel() != null) { return getVariableValue(acs.getFixedCasterLevel(), Constants.EMPTY_STRING).intValue(); } int tBonus = casterLev; boolean replaceCasterLevel = false; String tType; String tStr; // final List<TypedBonus> bonuses = new ArrayList<TypedBonus>(); final List<CasterLevelSpellBonus> bonuses = new ArrayList<>(); if (classOrRace != null) { // bonuses.addAll(getBonusesTo("CASTERLEVEL", classOrRace)); tBonus = (int) getTotalBonusTo("CASTERLEVEL", classOrRace); if (tBonus > 0) { tType = getSpellBonusType("CASTERLEVEL", classOrRace); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } // Support both types of syntax for CLASS: // BONUS:CASTERLEVEL|Sorcerer|1 and // BONUS:CASTERLEVEL|CLASS.Sorcerer|1 if (!classOrRace.startsWith("RACE.")) { tStr = "CLASS." + classOrRace; // bonuses.addAll( getBonusesTo("CASTERLEVEL", tStr) ); tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus > 0) { tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } } } if (aSpell == null) { return tallyCasterlevelBonuses(casterLev, replaceCasterLevel, bonuses); } if (!spellType.equals(Constants.NONE)) { tStr = "TYPE." + spellType; // bonuses.addAll( getBonusesTo("CASTERLEVEL", tStr) ); tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus > 0) { tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } tStr += ".RESET"; // final List<TypedBonus> reset = getBonusesTo("CASTERLEVEL", tStr); // if ( reset.size() > 0 ) // { // bonuses.addAll(reset); // replaceCasterLevel = true; // } tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus > 0) { replaceCasterLevel = true; tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } } tStr = "SPELL." + aSpell.getKeyName(); // bonuses.addAll( getBonusesTo("CASTERLEVEL", tStr) ); tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus > 0) { tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } tStr += ".RESET"; // final List<TypedBonus> reset = getBonusesTo("CASTERLEVEL", tStr); // if ( reset.size() > 0 ) // { // bonuses.addAll(reset); // replaceCasterLevel = true; // } tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus > 0) { replaceCasterLevel = true; tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } /* * This wraps in TreeSet because it looks to me like this is ordered * (given .RESET) */ for (SpellSchool school : new TreeSet<>(aSpell.getSafeListFor(ListKey.SPELL_SCHOOL))) { tStr = "SCHOOL." + school.toString(); // bonuses.addAll( getBonusesTo("CASTERLEVEL", tStr) ); tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus != 0) // Allow negative bonus to casterlevel { tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } tStr += ".RESET"; // final List<TypedBonus> reset1 = getBonusesTo("CASTERLEVEL", // tStr); // if ( reset.size() > 0 ) // { // bonuses.addAll(reset1); // replaceCasterLevel = true; // } tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus > 0) { replaceCasterLevel = true; tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } } for (String subschool : new TreeSet<>(aSpell.getSafeListFor(ListKey.SPELL_SUBSCHOOL))) { tStr = "SUBSCHOOL." + subschool; // bonuses.addAll( getBonusesTo("CASTERLEVEL", tStr) ); tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus > 0) { tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } tStr += ".RESET"; // final List<TypedBonus> reset1 = getBonusesTo("CASTERLEVEL", // tStr); // if ( reset.size() > 0 ) // { // bonuses.addAll(reset1); // replaceCasterLevel = true; // } tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus > 0) { replaceCasterLevel = true; tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } } //Not wrapped because it wasn't in 5.14 for (String desc : aSpell.getSafeListFor(ListKey.SPELL_DESCRIPTOR)) { tStr = "DESCRIPTOR." + desc; // bonuses.addAll( getBonusesTo("CASTERLEVEL", tStr) ); tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus > 0) { tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } tStr += ".RESET"; // final List<TypedBonus> reset1 = getBonusesTo("CASTERLEVEL", // tStr); // if ( reset.size() > 0 ) // { // bonuses.addAll(reset1); // replaceCasterLevel = true; // } tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus > 0) { replaceCasterLevel = true; tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } } final HashMapToList<CDOMList<Spell>, Integer> domainMap = getSpellLevelInfo(aSpell); if (domainMap != null) { for (CDOMList<Spell> spellList : domainMap.getKeySet()) { if (spellList instanceof DomainSpellList) { tStr = "DOMAIN." + spellList.getKeyName(); // bonuses.addAll( getBonusesTo("CASTERLEVEL", tStr) ); tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus > 0) { tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } tStr += ".RESET"; // final List<TypedBonus> reset1 = // getBonusesTo("CASTERLEVEL", tStr); // if ( reset.size() > 0 ) // { // bonuses.addAll(reset1); // replaceCasterLevel = true; // } tBonus = (int) getTotalBonusTo("CASTERLEVEL", tStr); if (tBonus > 0) { replaceCasterLevel = true; tType = getSpellBonusType("CASTERLEVEL", tStr); bonuses.add(new CasterLevelSpellBonus(tBonus, tType)); } } } } int result = tallyCasterlevelBonuses(casterLev, replaceCasterLevel, bonuses); return (result); } private static int tallyCasterlevelBonuses(final int casterLev, boolean replaceCasterLevel, final List<CasterLevelSpellBonus> bonuses) { // now go through all bonuses, checking types to see what should add // together for (int z = 0; z < bonuses.size() - 1; z++) { final CasterLevelSpellBonus zBonus = bonuses.get(z); String zType = zBonus.getType(); if ((zBonus.getBonus() == 0) || zType.isEmpty()) { continue; } boolean zReplace = false; boolean zStack = false; if (zType.endsWith(".REPLACE")) { zType = zType.substring(0, zType.length() - 8); zReplace = true; } else { if (zType.endsWith(".STACK")) { zType = zType.substring(0, zType.length() - 6); zStack = true; } } for (int k = z + 1; k < bonuses.size(); k++) { final CasterLevelSpellBonus kBonus = bonuses.get(k); String kType = kBonus.getType(); if ((kBonus.getBonus() == 0) || kType.isEmpty()) { continue; } boolean kReplace = false; boolean kStack = false; if (kType.endsWith(".REPLACE")) { kType = kType.substring(0, kType.length() - 8); kReplace = true; } else { if (kType.endsWith(".STACK")) { kType = kType.substring(0, kType.length() - 6); kStack = true; } } if (!zType.equals(kType)) { continue; } // if both end in ".REPLACE", add together and save for later // comparison if (zReplace && kReplace) { kBonus.setBonus(zBonus.getBonus() + kBonus.getBonus()); zBonus.setBonus(0); continue; } // if either ends in ".STACK", then they will add if (zStack || kStack) { continue; } // otherwise, only keep max if (zBonus.getBonus() > kBonus.getBonus()) { kBonus.setBonus(0); } else { zBonus.setBonus(0); } } } int result = 0; if (!replaceCasterLevel) { result += casterLev; } // result += TypedBonus.totalBonuses(bonuses); // Now go through bonuses and add it up for (CasterLevelSpellBonus resultBonus : bonuses) { result += resultBonus.getBonus(); } if (result <= 0) { result = 1; // Casterlevel must be at least 1 } return result; } private String getSpellBonusType(final String bonusType, final String bonusName) { return bonusManager.getSpellBonusType(bonusType, bonusName); } /** * returns all equipment (from the equipmentList) of type aString * * @param aList * @param aType * @return List */ public List<Equipment> addEqType(final List<Equipment> aList, final String aType) { for (Equipment eq : getEquipmentSet()) { if (eq.typeStringContains(aType) || aType.equalsIgnoreCase("CONTAINED") && (eq.getParent() != null)) { aList.add(eq); } } return aList; } /** * Adds a <tt>Kit</tt> to the applied list of kits for the character. * * @param aKit * The <tt>Kit</tt> to add. */ public void addKit(final Kit aKit) { kitFacet.add(id, aKit); setDirty(true); } /** * @param acs * is the CharacterSpell object containing the spell which is to * be modified * @param aFeatList * is the list of feats to be added to the SpellInfo object * @param classKey * is the name of the class whose list of character spells will * be modified * @param bookName * is the name of the book for the SpellInfo object * @param spellLevel * is the original (unadjusted) level of the spell not including * feat adjustments * @param adjSpellLevel * is the adjustedLevel (including feat adjustments) of this * spell, it may be higher if the user chooses a higher level. * * @return an empty string on successful completion, otherwise the return * value indicates the reason the add function failed. */ public String addSpell(CharacterSpell acs, final List<Ability> aFeatList, final String classKey, final String bookName, final int adjSpellLevel, final int spellLevel) { if (acs == null) { return "Invalid parameter to add spell"; } PCClass aClass = null; final Spell aSpell = acs.getSpell(); if ((bookName == null) || (bookName.isEmpty())) { return "Invalid spell list/book name."; } if (!hasSpellBook(bookName)) { return "Could not find spell list/book " + bookName; } if (classKey != null) { aClass = getClassKeyed(classKey); if ((aClass == null) && (classKey.lastIndexOf('(') >= 0)) { aClass = getClassKeyed(classKey.substring(0, classKey.lastIndexOf('(')).trim()); } } // If this is a spellbook, the class doesn't have to be one the PC has // already. SpellBook spellBook = getSpellBookByName(bookName); if (aClass == null && spellBook.getType() == SpellBook.TYPE_SPELL_BOOK) { aClass = Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(PCClass.class, classKey); if ((aClass == null) && (classKey.lastIndexOf('(') >= 0)) { aClass = Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(PCClass.class, classKey.substring(0, classKey.lastIndexOf('(')).trim()); } } if (aClass == null) { return "No class keyed " + classKey; } if (!aClass.getSafe(ObjectKey.MEMORIZE_SPELLS) && !bookName.equals(Globals.getDefaultSpellBook())) { return aClass.getDisplayName() + " can only add to " + Globals.getDefaultSpellBook(); } // Divine spellcasters get no bonus spells at level 0 // TODO: allow classes to define how many bonus spells they get each // level! // int numSpellsFromSpecialty = aClass.getNumSpellsFromSpecialty(); // if (spellLevel == 0 && // "Divine".equalsIgnoreCase(aClass.getSpellType())) // { // numSpellsFromSpecialty = 0; // } // all the exists checks are done. // don't allow adding spells which are not qualified for. if (!aSpell.qualifies(this, aSpell)) { return "You do not qualify for " + acs.getSpell().getDisplayName() + "."; } // don't allow adding spells which are prohibited to known // or prepared lists // But if a spell is both prohibited and in a speciality // which can be the case for some spells, then allow it. if (spellBook.getType() != SpellBook.TYPE_SPELL_BOOK && !acs.isSpecialtySpell(this) && SpellCountCalc.isProhibited(aSpell, aClass, this)) { return acs.getSpell().getDisplayName() + " is prohibited."; } // Now let's see if they should be able to add this spell // first check for known/cast/threshold final int known = this.getSpellSupport(aClass).getKnownForLevel(spellLevel, this); int specialKnown = 0; final int cast = this.getSpellSupport(aClass).getCastForLevel(adjSpellLevel, bookName, true, true, this); SpellCountCalc.memorizedSpellForLevelBook(this, aClass, adjSpellLevel, bookName); final boolean isDefault = bookName.equals(Globals.getDefaultSpellBook()); if (isDefault) { specialKnown = this.getSpellSupport(aClass).getSpecialtyKnownForLevel(spellLevel, this); } int numPages = 0; // known is the maximum spells that can be known this level // listNum is the current spells already memorized this level // cast is the number of spells that can be cast at this level // Modified this to use new availableSpells() method so you can "blow" // higher-level slots on // lower-level spells // in re BUG [569517] // sk4p 13 Dec 2002 if (spellBook.getType() == SpellBook.TYPE_SPELL_BOOK) { // If this is a spellbook rather than known spells // or prepared spells, then let them add spells up to // the page limit of the book. // Explicitly should *not* set the dirty flag to true. spellLevelTemp = spellLevel; /* * TODO Need to understand more about this context of formula * resolution (in context of a spell??) in order to understand how * to put this method into the Formula interface */ numPages = getVariableValue(acs, spellBook.getPageFormula().toString(), "").intValue(); // Check number of pages remaining in the book if (numPages + spellBook.getNumPagesUsed() > spellBook.getNumPages()) { return "There are not enough pages left to add this spell to the spell book."; } spellBook.setNumPagesUsed(numPages + spellBook.getNumPagesUsed()); spellBook.setNumSpells(spellBook.getNumSpells() + 1); } else if (!aClass.getSafe(ObjectKey.MEMORIZE_SPELLS) && !availableSpells(adjSpellLevel, aClass, bookName, true, acs.isSpecialtySpell(this))) { String ret; int maxAllowed; // If this were a specialty spell, would there be room? if (!acs.isSpecialtySpell(this) && availableSpells(adjSpellLevel, aClass, bookName, true, true)) { ret = "Your remaining slot(s) must be filled with your speciality."; maxAllowed = known; } else { ret = "You can only learn " + (known + specialKnown) + " spells for level " + adjSpellLevel + " \nand there are no higher-level slots available."; maxAllowed = known + specialKnown; } int memTot = SpellCountCalc.memorizedSpellForLevelBook(this, aClass, adjSpellLevel, bookName); int spellDifference = maxAllowed - memTot; if (spellDifference > 0) { ret += "\n" + spellDifference + " spells from lower levels are using slots for this level."; } return ret; } else if (aClass.getSafe(ObjectKey.MEMORIZE_SPELLS) && !isDefault && !availableSpells(adjSpellLevel, aClass, bookName, false, acs.isSpecialtySpell(this))) { String ret; int maxAllowed; if (!acs.isSpecialtySpell(this) && availableSpells(adjSpellLevel, aClass, bookName, false, true)) { ret = "Your remaining slot(s) must be filled with your speciality or domain."; maxAllowed = this.getSpellSupport(aClass).getCastForLevel( adjSpellLevel, bookName, false, true, this); } else if (acs.isSpecialtySpell(this) && availableSpells(adjSpellLevel, aClass, bookName, false, false)) { ret = "Your remaining slot(s) must be filled with spells not from your speciality or domain."; maxAllowed = this.getSpellSupport(aClass).getCastForLevel( adjSpellLevel, bookName, false, true, this); } else { ret = "You can only prepare " + cast + " spells for level " + adjSpellLevel + " \nand there are no higher-level slots available."; maxAllowed = cast; int memTot = SpellCountCalc.memorizedSpellForLevelBook(this, aClass, adjSpellLevel, bookName); int spellDifference = maxAllowed - memTot; if (spellDifference > 0) { ret += "\n" + spellDifference + " spells from lower levels are using slots for this level."; } } return ret; } // determine if this spell already exists // for this character in this book at this level SpellInfo si = null; final List<CharacterSpell> acsList = getCharacterSpells(aClass, acs.getSpell(), bookName, adjSpellLevel); if (!acsList.isEmpty()) { for (int x = acsList.size() - 1; x >= 0; x--) { final CharacterSpell c = acsList.get(x); if (!c.equals(acs)) { acsList.remove(x); } } } final boolean isEmpty = acsList.isEmpty(); if (!isEmpty) { // I am not sure why this code is set up like this but it is // bogus. I am trying to break as little as possible so if // I have one matching spell I will use it otherwise I will // use the passed in spell. if (acsList.size() == 1) { final CharacterSpell tcs = acsList.get(0); si = tcs.getSpellInfoFor(bookName, adjSpellLevel, aFeatList); } else { si = acs.getSpellInfoFor(bookName, adjSpellLevel, aFeatList); } } if (si != null) { // ok, we already known this spell, so if they are // trying to add it to the default spellBook, barf // otherwise increment the number of times memorised if (isDefault) { return "The Known Spells spellbook contains all spells of this level that you know. You cannot place spells in multiple times."; } si.setTimes(si.getTimes() + 1); } else { if (isEmpty && !containsCharacterSpell(aClass, acs)) { addCharacterSpell(aClass, acs); } else if (isEmpty) { // Make sure that we are working on the same spell object, not just the same spell for (CharacterSpell characterSpell : getCharacterSpells(aClass)) { if (characterSpell.equals(acs)) { acs = characterSpell; } } } si = acs.addInfo(spellLevel, adjSpellLevel, 1, bookName, aFeatList); } // Set number of pages on the spell si.setNumPages(si.getNumPages() + numPages); setDirty(true); return ""; } /** * return value indicates if book was actually added or not * * @param aName * @return TRUE or FALSE */ public boolean addSpellBook(final String aName) { if (aName != null && (!aName.isEmpty()) && !spellBookFacet.containsBookNamed(id, aName)) { return addSpellBook(new SpellBook(aName, SpellBook.TYPE_PREPARED_LIST)); } return false; } public boolean addSpellBook(final SpellBook book) { if (!spellBookFacet.containsBookNamed(id, book.getName())) { spellBookFacet.add(id, book); //setDirty(true); return true; } return false; } public boolean addTemplate(final PCTemplate inTemplate) { if (inTemplate == null) { return false; } // Don't allow multiple copies of template. if (hasTemplate(inTemplate)) { return false; } int lockMonsterSkillPoints = 0; // this is what this value was before // adding this template for (PCClass pcClass : getClassSet()) { if (pcClass.isMonster()) { lockMonsterSkillPoints = (int) getTotalBonusTo("MONSKILLPTS", "LOCKNUMBER"); break; } } boolean added = templateInputFacet.add(id, inTemplate); if (!added) { return false; } this.setDirty(true); calcActiveBonuses(); int postLockMonsterSkillPoints; // this is what this value was before // adding this template boolean first = true; for (PCClass pcClass : getClassSet()) { if (pcClass.isMonster()) { postLockMonsterSkillPoints = (int) getTotalBonusTo("MONSKILLPTS", "LOCKNUMBER"); if (postLockMonsterSkillPoints != lockMonsterSkillPoints && postLockMonsterSkillPoints > 0) { for (PCLevelInfo pi : getLevelInfo()) { final int newSkillPointsGained = recalcSkillPointMod(pcClass, pi.getClassLevel()); if (pi.getClassKeyName().equals(pcClass.getKeyName())) { final int formerGained = pi.getSkillPointsGained(this); pi.setSkillPointsGained(this, newSkillPointsGained); pi.setSkillPointsRemaining(pi.getSkillPointsRemaining() + newSkillPointsGained - formerGained); setSkillPool(pcClass, pcClass.getSkillPool(this) + newSkillPointsGained - formerGained); } } } } // // Recalculate HPs in case HD have changed. // if (!importing) { Processor<HitDie> dieLock = inTemplate.get(ObjectKey.HITDIE); if (dieLock != null) { for (int level = 1; level <= getLevel(pcClass); level++) { HitDie baseHD = pcClass.getSafe(ObjectKey.LEVEL_HITDIE); if (!baseHD.equals(getLevelHitDie(pcClass, level))) { // If the HD has changed from base reroll rollHP(pcClass, level, first); } } } } first = false; } setDirty(true); return true; } public void adjustGold(final double delta) { goldFacet.adjustGold(id, delta); setDirty(true); } /** * recalculate all the move rates and modifiers */ public void adjustMoveRates() { moveResultFacet.reset(id); //setDirty(true); } public List<Spell> aggregateSpellList(final String school, final String subschool, final String descriptor, final int minLevel, final int maxLevel) { final List<Spell> retList = new ArrayList<>(); for (PObject pObj : getSpellClassList()) { for (int a = minLevel; a <= maxLevel; a++) { for (CharacterSpell cs : getCharacterSpells(pObj, a)) { final Spell aSpell = cs.getSpell(); SpellSchool ss = Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(SpellSchool.class, school); if ((school.isEmpty()) || (ss != null) && aSpell.containsInList(ListKey.SPELL_SCHOOL, ss) || (subschool.isEmpty()) || aSpell.containsInList(ListKey.SPELL_SUBSCHOOL, subschool) || (descriptor.isEmpty()) || aSpell.containsInList(ListKey.SPELL_DESCRIPTOR, descriptor)) { retList.add(aSpell); } } } } return retList; } /** * @return Total base attack bonus as an int */ public int baseAttackBonus() { // check for cached version final String cacheLookup = "BaseAttackBonus"; Float total = variableProcessor.getCachedVariable(cacheLookup); if (total != null) { return total.intValue(); } // get Master's BAB final PlayerCharacter nPC = getMasterPC(); if ((nPC != null) && (!masterFacet.getCopyMasterBAB(id).isEmpty())) { int masterBAB = nPC.baseAttackBonus(); final String copyMasterBAB = replaceMasterString(masterFacet.getCopyMasterBAB(id), masterBAB); masterBAB = getVariableValue(copyMasterBAB, "").intValue(); variableProcessor.addCachedVariable(cacheLookup, (float) masterBAB); return masterBAB; } int bab = (int) getTotalBonusTo("COMBAT", "BASEAB"); variableProcessor.addCachedVariable(cacheLookup, (float) bab); return bab; } /** * Creates the activeBonusList which is used to calculate all the bonuses to * a PC */ public void calcActiveBonuses() { if (importing || (getRace() == null)) { return; } // Keep rebuilding the active bonus map until the // contents do not change. This is to cope with the // situation where we have a variable A that has a prereq // that depends on variable B that will not be the correct // value until after the map has been completely created. int count = 0; do { if (count >= 29) { Logging .errorPrint("Active bonus loop exceeded reasonable limit of " + count + "."); bonusManager.logChangeFromCheckpoint(); if (count > 31) { break; } } bonusManager.checkpointBonusMap(); setDirty(true); count++; calcActiveBonusLoop(); if (Globals.checkRule(RuleConstants.RETROSKILL)) { checkSkillModChange(); } } while (!bonusManager.compareToCheckpoint()); // If the newly calculated bonus map is different to the old one // loop again until they are the same. if (Logging.isDebugMode()) { Logging.log(Logging.DEBUG, "Ran " + count + " loops to calc bonuses"); } } /* * These are designed to catch a re-entrant bonus loop, which can occur * when a BONUS contains a level limited item in a Formula, such as BAB */ private int cablInt = 1; private int lastCablInt = 0; private void calcActiveBonusLoop() { if (cablInt == lastCablInt) { return; } lastCablInt = cablInt; bonusManager.setActiveBonusList(); // buildBonusMap(bonuses); bonusManager.buildActiveBonusMap(); cablInt++; bonusChangeFacet.reset(id); } public int calcSR(final boolean includeEquipment) { int SR = srFacet.getSR(id); if (includeEquipment) { for (Equipment eq : getEquippedEquipmentSet()) { SR = Math.max(SR, eq.getSafe(ObjectKey.SR).getReduction().resolve(this, eq.getQualifiedKey()) .intValue()); for (EquipmentModifier eqMod : eq.getEqModifierList(true)) { SR = Math.max(SR, eqMod.getSR(eq, this)); } for (EquipmentModifier eqMod : eq.getEqModifierList(false)) { SR = Math.max(SR, eqMod.getSR(eq, this)); } } } SR += (int) getTotalBonusTo("MISC", "SR"); // SR += (int) getBonusValue("MISC", "SR"); // // This would make more sense to just not add in the first place... // if (!includeEquipment) { SR -= (int) getEquipmentBonusTo("MISC", "SR"); } return SR; } /** * Method will go through the list of classes that the PC has and see if * they can cast spells of desired type at desired <b>spell level</b>. * * @param spellType * Spell type to check for * @param spellLevel * Desired spell level * @param minNumSpells * Minimum number of spells at the desired spell level * @return boolean <p> author David Wilson * <eldiosyeldiablo@users.sourceforge.net> */ private boolean canCastSpellTypeLevel(final String spellType, final int spellLevel, final int minNumSpells) { for (PCClass aClass : getClassSet()) { FactKey<String> fk = FactKey.valueOf("SpellType"); String classSpellType = aClass.getResolved(fk); if (classSpellType != null && ("Any".equalsIgnoreCase(spellType) || classSpellType.equalsIgnoreCase(spellType))) { // Get the number of known spells for the level int knownForLevel = this.getSpellSupport(aClass).getKnownForLevel(spellLevel, this); knownForLevel += this.getSpellSupport(aClass).getSpecialtyKnownForLevel(spellLevel, this); if (knownForLevel >= minNumSpells) { return true; } // See if the character can cast // at the required spell level if (this.getSpellSupport(aClass).getCastForLevel(spellLevel, this) >= minNumSpells) { return true; } // If they don't memorise spells and don't have // a CastList then they use something funky // like Power Points (psionic) if (!aClass.getSafe(ObjectKey.MEMORIZE_SPELLS) && !this.getSpellSupport(aClass).hasKnownList() && this.getSpellSupport(aClass).canCastSpells(this)) { return true; } } } return false; } /** * Method will go through the list of classes that the PC has and see if * they can cast spells of desired type at desired <b>spell level</b>. * * @param spellType * Spell type to check for * @param spellLevel * Desired spell level * @return The number of spells castable **/ public int countSpellCastTypeLevel(final String spellType, final int spellLevel) { int known = 0; int cast = 0; for (PCClass aClass : getClassSet()) { FactKey<String> fk = FactKey.valueOf("SpellType"); String classSpellType = aClass.getResolved(fk); if (classSpellType != null && ("Any".equalsIgnoreCase(spellType) || classSpellType.equalsIgnoreCase(spellType))) { int numCastLevel = this.getSpellSupport(aClass).getCastForLevel(spellLevel, this); // Get the number of known spells for the level known += this.getSpellSupport(aClass).getKnownForLevel(spellLevel, this); if (numCastLevel > 0) { known += this.getSpellSupport(aClass).getSpecialtyKnownForLevel(spellLevel, this); } // See if the character can cast // at the required spell level cast += numCastLevel; // If they don't memorise spells and don't have // a CastList then they use something funky // like Power Points (psionic) if (!aClass.getSafe(ObjectKey.MEMORIZE_SPELLS) && !this.getSpellSupport(aClass).hasKnownList() && this.getSpellSupport(aClass).canCastSpells(this)) { return Integer.MAX_VALUE; } } } return known == 0 ? cast : known; } /** * Check whether a deity can be selected by this character * * @return {@code true} means the deity can be a selected by a * character with the given properties; {@code false} means * the character cannot. */ public boolean canSelectDeity(final Deity aDeity) { return legalDeityFacet.allows(id, aDeity); } /** * Return value indicates whether or not a spell was deleted. * * @param si * @param aClass * @param bookName * @return String */ public String delSpell(SpellInfo si, final PCClass aClass, final String bookName) { if ((bookName == null) || (bookName.isEmpty())) { return "Invalid spell book name."; } if (aClass == null) { return "Error: Class is null"; } final CharacterSpell acs = si.getOwner(); final boolean isDefault = bookName.equals(Globals.getDefaultSpellBook()); // yes, you can remove spells from the default spellbook, // but they will just get added back in when the character // is re-loaded. But, allow them to do it anyway, just in case // there is some weird spell that keeps getting loaded by // accident (or is saved in the .pcg file) if (isDefault && this.getSpellSupport(aClass).isAutoKnownSpell(acs.getSpell(), si.getActualLevel(), false, this)) { Logging.errorPrint("Notice: removing " + acs.getSpell().getDisplayName() + " even though it is an auto known spell"); } SpellBook spellBook = getSpellBookByName(bookName); if (spellBook.getType() == SpellBook.TYPE_SPELL_BOOK) { int pagesPerSpell = si.getNumPages() / si.getTimes(); spellBook.setNumPagesUsed(spellBook.getNumPagesUsed() - pagesPerSpell); spellBook.setNumSpells(spellBook.getNumSpells() - 1); si.setNumPages(si.getNumPages() - pagesPerSpell); } si.setTimes(si.getTimes() - 1); if (si.getTimes() <= 0) { acs.removeSpellInfo(si); } // Remove the spell form the character's class instance if it // is no longer present in any book if (acs.getInfoList().isEmpty()) { removeCharacterSpell(aClass, acs); } return ""; } /** * Calculate different kinds of bonuses to saves. possible tokens are * <ul> * <li>save</li> * <li>save.TOTAL</li> * <li>save.BASE</li> * <li>save.MISC</li> * <li>save.list</li> * <li>save.TOTAL.list</li> * <li>save.BASE.list</li> * <li>save.MISC.list</li> * </ul> * where<br> * save := "CHECK1"|"CHECK2"|"CHECK3"<br> * list := ((include|exclude)del)*(include|exclude)<br> * include := "FEATS"|"MAGIC"|"RACE"<br> * exclude := "NOFEATS"|"NOMAGIC"|"NORACE"|"NOSTAT" <br> * del := "." <br> * given as regular expression. <p> "include"-s will add the appropriate * modifier "exclude"-s will subtract the appropriate modifier <p> (This * means <tt>save.MAGIC.NOMAGIC</tt> equals 0, whereas * <tt>save.RACE.RACE</tt> equals 2 times the racial bonus) <p> If you * use unrecognised terminals, their value will amount to 0 This means * <tt>save.BLABLA</tt> equals 0 whereas <tt>save.MAGIC.BLABLA</tt> * equals <tt>save.MAGIC</tt> <p> <br> * author: Thomas Behr 09-03-02 * * @param check * @param tokenString * tokenString to parse * @return the calculated save bonus */ public int calculateSaveBonus(final PCCheck check, final String tokenString) { if (check == null) { return 0; } final StringTokenizer aTok = new StringTokenizer(tokenString, "."); final String[] tokens = new String[aTok.countTokens()]; int save = 0; String saveType = check.toString(); for (int i = 0; aTok.hasMoreTokens(); ++i) { tokens[i] = aTok.nextToken(); if ("TOTAL".equals(tokens[i])) { save += getTotalCheck(check); } else if ("BASE".equals(tokens[i])) { save += getBaseCheck(check); } else if ("MISC".equals(tokens[i])) { String saveVar = ControlUtilities .getControlToken(Globals.getContext(), CControl.MISCSAVE); if (saveVar == null) { save += (int) getTotalBonusTo("SAVE", saveType); } else { save += ((Number) getLocal(check, saveVar)).intValue(); } } if ("EPIC".equals(tokens[i])) { String saveVar = ControlUtilities .getControlToken(Globals.getContext(), CControl.EPICSAVE); if (saveVar == null) { save += (int) getBonusDueToType("SAVE", saveType, "EPIC"); } else { save += ((Number) getLocal(check, saveVar)).intValue(); } } if ("MAGIC".equals(tokens[i])) { String saveVar = ControlUtilities .getControlToken(Globals.getContext(), CControl.MAGICSAVE); if (saveVar == null) { save += (int) getEquipmentBonusTo("SAVE", saveType); } else { save += ((Number) getLocal(check, saveVar)).intValue(); } } if ("RACE".equals(tokens[i])) { String saveVar = ControlUtilities .getControlToken(Globals.getContext(), CControl.RACESAVE); if (saveVar == null) { save += calculateSaveBonusRace(check); } else { save += ((Number) getLocal(check, saveVar)).intValue(); } } if ("FEATS".equals(tokens[i])) { if (ControlUtilities.hasControlToken(Globals.getContext(), CControl.BASESAVE)) { Logging .errorPrint("FEATS is not a supported SAVE modification " + "when BASESAVE Code Control is used"); } else { save += (int) getFeatBonusTo("SAVE", saveType); } } if ("STATMOD".equals(tokens[i])) { String saveVar = ControlUtilities .getControlToken(Globals.getContext(), CControl.STATMODSAVE); if (saveVar == null) { save += (int) checkBonusFacet.getCheckBonusTo(id, "SAVE", saveType); } else { save += ((Number) getLocal(check, saveVar)).intValue(); } } /* * exclude stuff */ if ("NOEPIC".equals(tokens[i])) { String saveVar = ControlUtilities .getControlToken(Globals.getContext(), CControl.EPICSAVE); if (saveVar == null) { save -= (int) getBonusDueToType("SAVE", saveType, "EPIC"); } else { save -= ((Number) getLocal(check, saveVar)).intValue(); } } if ("NOMAGIC".equals(tokens[i])) { String saveVar = ControlUtilities .getControlToken(Globals.getContext(), CControl.MAGICSAVE); if (saveVar == null) { save -= (int) getEquipmentBonusTo("SAVE", saveType); } else { save -= ((Number) getLocal(check, saveVar)).intValue(); } } if ("NORACE".equals(tokens[i])) { String saveVar = ControlUtilities .getControlToken(Globals.getContext(), CControl.RACESAVE); if (saveVar == null) { save -= calculateSaveBonusRace(check); } else { save -= ((Number) getLocal(check, saveVar)).intValue(); } } if ("NOFEATS".equals(tokens[i])) { if (ControlUtilities.hasControlToken(Globals.getContext(), CControl.BASESAVE)) { Logging .errorPrint("NOFEATS is not a supported SAVE modification " + "when BASESAVE Code Control is used"); } else { save -= (int) getFeatBonusTo("SAVE", saveType); } } if ("NOSTAT".equals(tokens[i]) || "NOSTATMOD".equals(tokens[i])) { String saveVar = ControlUtilities .getControlToken(Globals.getContext(), CControl.STATMODSAVE); if (saveVar == null) { save -= (int) checkBonusFacet.getCheckBonusTo(id, "SAVE", saveType); } else { save -= ((Number) getLocal(check, saveVar)).intValue(); } } } return save; } /** * return value indicates whether or not a book was actually removed * * @param aName * @return true or false */ public boolean delSpellBook(final String aName) { if ((!aName.isEmpty()) && !aName.equals(Globals.getDefaultSpellBook()) && spellBookFacet.containsBookNamed(id, aName)) { processSpellBookRemoval(aName); return true; } return false; } private void processSpellBookRemoval(String aName) { spellBookFacet.removeBookNamed(id, aName); setDirty(true); for (PCClass pcClass : getClassSet()) { for (CharacterSpell cs : getCharacterSpells(pcClass, aName)) { cs.removeSpellInfo(cs.getSpellInfoFor(aName, -1)); } } } private void determinePrimaryOffWeapon() { primaryWeaponFacet.removeAll(id); secondaryWeaponFacet.removeAll(id); if (!hasEquipment()) { return; } final List<Equipment> unequippedPrimary = new ArrayList<>(); final List<Equipment> unequippedSecondary = new ArrayList<>(); for (Equipment eq : getEquipmentSet()) { if (!eq.isWeapon() || (eq.getSlots(this) < 1)) { continue; } final boolean isEquipped = eq.isEquipped(); if ((eq.getLocation() == EquipmentLocation.EQUIPPED_PRIMARY) || ((eq.getLocation() == EquipmentLocation.EQUIPPED_BOTH) && primaryWeaponFacet.isEmpty(id)) || (eq.getLocation() == EquipmentLocation.EQUIPPED_TWO_HANDS)) { if (isEquipped) { primaryWeaponFacet.add(id, eq); } else { unequippedPrimary.add(eq); } } else if ((eq.getLocation() == EquipmentLocation.EQUIPPED_BOTH) && !primaryWeaponFacet.isEmpty(id)) { if (isEquipped) { secondaryWeaponFacet.add(id, eq); } else { unequippedSecondary.add(eq); } } if (eq.getLocation() == EquipmentLocation.EQUIPPED_SECONDARY) { if (isEquipped) { secondaryWeaponFacet.add(id, eq); } else { unequippedSecondary.add(eq); } } if (eq.getLocation() == EquipmentLocation.EQUIPPED_TWO_HANDS) { for (int y = 0; y < (eq.getNumberEquipped() - 1); ++y) { if (isEquipped) { secondaryWeaponFacet.add(id, eq); } else { unequippedSecondary.add(eq); } } } } if (Globals.checkRule(RuleConstants.EQUIPATTACK)) { if (!unequippedPrimary.isEmpty()) { primaryWeaponFacet.addAll(id, unequippedPrimary); } if (!unequippedSecondary.isEmpty()) { secondaryWeaponFacet.addAll(id, unequippedSecondary); } } } public boolean hasMadeKitSelectionForAgeSet(final int index) { return ((index >= 0) && (index < 10) && ageSetKitSelections[index]); } public boolean hasSpecialAbility(final String abilityKey) { for (SpecialAbility sa : getSpecialAbilityList()) { if (sa.getKeyName().equalsIgnoreCase(abilityKey)) { return true; } } return false; } public int hitPoints() { int total = 0; String aString = SettingsHandler.getGame().getHPFormula(); if (!aString.isEmpty()) { for (;;) { int startIdx = aString.indexOf("$$"); if (startIdx < 0) { break; } int endIdx = aString.indexOf("$$", startIdx + 2); if (endIdx < 0) { break; } String lookupString = aString.substring(startIdx + 2, endIdx); lookupString = pcgen.io.ExportHandler.getTokenString(this, lookupString); aString = aString.substring(0, startIdx) + lookupString + aString.substring(endIdx + 2); } total = getVariableValue(aString, "").intValue(); } else { final double iConMod = getStatBonusTo("HP", "BONUS"); for (PCClass pcClass : getClassSet()) { total += getClassHitPoints(pcClass, (int) iConMod); } } total += (int) getTotalBonusTo("HP", "CURRENTMAX"); // // now we see if this PC is a Familiar final PlayerCharacter nPC = getMasterPC(); if (nPC == null) { return total; } if (masterFacet.getCopyMasterHP(id).isEmpty()) { return total; } // // In order for the BONUS's to work, the PC we want // to get the hit points for must be the "current" one. // int masterHP = nPC.hitPoints(); final String copyMasterHP = replaceMasterString(masterFacet.getCopyMasterHP(id), masterHP); masterHP = getVariableValue(copyMasterHP, "").intValue(); return masterHP; } private int getClassHitPoints(PCClass pcClass, int iConMod) { int total = 0; for (int i = 0; i <= getLevel(pcClass); ++i) { PCClassLevel pcl = getActiveClassLevel(pcClass, i); Integer hp = getHP(pcl); if (hp != null && hp > 0) { int iHp = hp + iConMod; if (iHp < 1) { iHp = 1; } total += iHp; } } return total; } /** * Change the number of levels a character has in a particular class. Note: * It is assumed that this method is not used as part of loading a * previously saved character. there is no way to bypass the prerequisites * with this method, also this method does not print warning messages see: * incrementClassLevel(int, PCClass, boolean, boolean); * * @param mod * the number of levels to add/remove * @param aClass * the class to adjust */ public void incrementClassLevel(final int mod, final PCClass aClass) { incrementClassLevel(mod, aClass, false); setDirty(true); } /** * Calculates the number of languages that the character is qualified * for. * * @return The number of languages allowed */ public int getBonusLanguageCount() { int i = Math.max(0, (int) getStatBonusTo("LANG", "BONUS")); if (getRace() != null) { i = (int) (i + getTotalBonusTo("LANGUAGES", "NUMBER")); } return i; } /** * Lists all the tokens that match prefix with associated values * * @param bonusType * @param bonusName * @return String TODO - Not sure what this is trying to do. */ public String listBonusesFor(String bonusType, String bonusName) { return bonusManager.listBonusesFor(bonusType, bonusName); } public boolean loadDescriptionFilesInDirectory(final String aDirectory) { new File(aDirectory).list((dir, name) -> { final File descriptionFile = new File(dir, name); if (PCGFile.isPCGenListFile(descriptionFile)) { BufferedReader descriptionReader = null; try { if (descriptionFile.exists()) { final char[] inputLine; // final BufferedReader descriptionReader = new // BufferedReader(new FileReader(descriptionFile)); descriptionReader = new BufferedReader(new InputStreamReader(new FileInputStream( descriptionFile), "UTF-8")); final int length = (int) descriptionFile.length(); inputLine = new char[length]; descriptionReader.read(inputLine, 0, length); setDescriptionLst(getDescriptionLst() + new String(inputLine)); } } catch (IOException exception) { Logging.errorPrint("IOException in PlayerCharacter.loadDescriptionFilesInDirectory", exception); } finally { if (descriptionReader != null) { try { descriptionReader.close(); } catch (IOException e) { Logging.errorPrint( "Couldn't close descriptionReader in PlayerCharacter.loadDescriptionFilesInDirectory", e); // Not much to do... } } } } else if (dir.isDirectory()) { loadDescriptionFilesInDirectory(dir.getPath() + File.separator + name); } return false; }); return false; } public void makeIntoExClass(final PCClass fromClass) { CDOMSingleRef<PCClass> exc = fromClass.get(ObjectKey.EX_CLASS); try { PCClass cl = exc.get(); PCClass toClass = getClassKeyed(cl.getKeyName()); boolean bClassNew; int toLevel; if (toClass == null) { toClass = cl.clone(); //Still required :( bClassNew = true; toLevel = 0; } else { bClassNew = false; toLevel = getLevel(toClass); } //Capture necessary information final int fromLevel = getLevel(fromClass); Integer[] hpArray = new Integer[fromLevel]; for (int i = 0; i < fromLevel; i++) { PCClassLevel frompcl = getActiveClassLevel(fromClass, i); Integer hp = getHP(frompcl); if (hp == null) { System.err.println("Did not find HP for " + fromClass + " " + (i + 1) + " " + frompcl); } hpArray[i] = hp; } for (int i = 0; i < fromLevel; i++) { fromClass.doMinusLevelMods(this, fromLevel - i); } fromClass.setLevel(0, this); //Do the class swap if (bClassNew) { classFacet.replaceClass(id, fromClass, toClass); } else { classFacet.removeClass(id, fromClass); } toClass.setLevel(toLevel + fromLevel, this); //Restore capture info to new class for (int i = 0; i < fromLevel; i++) { PCClassLevel topcl = getActiveClassLevel(toClass, i); setHP(topcl, hpArray[i]); } // // change all the levelling info to the ex-class as well // for (int idx = getLevelInfoSize() - 1; idx >= 0; --idx) { final PCLevelInfo li = levelInfoFacet.get(id, idx); if (li.getClassKeyName().equals(fromClass.getKeyName())) { li.setClassKeyName(toClass.getKeyName()); } } // // Find all skills associated with old class and link them to new // class // for (Skill skill : getSkillSet()) { SkillRankControl.replaceClassRank(this, skill, fromClass, cl); } setSkillPool(toClass, fromClass.getSkillPool(this)); } catch (NumberFormatException nfe) { ShowMessageDelegate .showMessageDialog(nfe.getMessage(), Constants.APPLICATION_NAME, MessageType.INFORMATION); } } public int minXPForECL() { return levelTableFacet.minXPForLevel(levelFacet.getECL(id), id); } public int minXPForNextECL() { return levelTableFacet.minXPForLevel(levelFacet.getECL(id) + 1, id); } /** * Figure out if Load should affect AC and Skills, if so, set the load * appropriately, otherwise set a light load to eliminate the effects of * heavier loads * * @return a loadType appropriate for this Pc */ private Load getHouseRuledLoadType() { if (Globals.checkRule(RuleConstants.SYS_LDPACSK)) { return getLoadType(); } return Load.LIGHT; } /** * Calculate the AC bonus from equipped items. Extracted from * modToFromEquipment. * * @return PC's AC bonus from equipment */ public int modToACFromEquipment() { int bonus = 0; for (Equipment eq : getEquippedEquipmentSet()) { bonus += eq.getACMod(this).intValue(); } return bonus; } /** * Calculate the ACCHECK bonus from equipped items. Extracted from * modToFromEquipment. * * TODO Penalty for load could/should be GameMode specific? * * @return PC's ACCHECK bonus from equipment * @deprecated due to PCACCHECK code control */ @Deprecated public int processOldAcCheck() { Load load = getHouseRuledLoadType(); int bonus = 0; int penaltyForLoad = (load == Load.MEDIUM) ? -3 : (load == Load.HEAVY) ? -6 : 0; final IdentityList<Equipment> vEqList = new IdentityList<>(tempBonusItemList); for (Equipment eq : getEquippedEquipmentSet()) { // Do not count virtual items created by temporary bonuses if (!vEqList.contains(eq)) { bonus += EqToken.getAcCheckTokenInt(this, eq); } } bonus = Math.min(bonus, penaltyForLoad); bonus += (int) getTotalBonusTo("MISC", "ACCHECK"); return bonus; } /** * Calculate the MAXDEX bonus taking account of equipped items. Extracted * from modToFromEquipment. * * @return MAXDEX bonus * @deprecated due to PCMAXDEX code control */ @Deprecated public int processOldMaxDex() { final int statBonus = (int) getStatBonusTo("MISC", "MAXDEX"); final Load load = getHouseRuledLoadType(); int bonus = (load == Load.MEDIUM) ? 3 : (load == Load.HEAVY) ? 1 : (load == Load.OVERLOAD) ? 0 : statBonus; // If this is still true after all the equipment has been // examined, then we should use the Maximum - Maximum Dex modifier. boolean useMax = (load == Load.LIGHT); for (Equipment eq : getEquippedEquipmentSet()) { final int potentialMax = EqToken.getMaxDexTokenInt(this, eq); if (potentialMax != Constants.MAX_MAXDEX) { if (useMax || bonus > potentialMax) { bonus = potentialMax; } useMax = false; } } if (useMax) { bonus = Constants.MAX_MAXDEX; } bonus += ((int) getTotalBonusTo("MISC", "MAXDEX") - statBonus); if (bonus < 0) { bonus = 0; } else if (bonus > Constants.MAX_MAXDEX) { bonus = Constants.MAX_MAXDEX; } return bonus; } /** * Takes a String and a Class name and computes spell based variable such as * Class level. * * @param aSpell The spell object * @param aString the variable to evaluate * @return String */ public String parseSpellString(final CharacterSpell aSpell, String aString) { String aSpellClass = aSpell.getVariableSource(this); if (aSpellClass.isEmpty()) { return aString; } // Only want to replace items between ()'s while (aString.lastIndexOf('(') >= 0) { boolean found = false; final int start = aString.indexOf('('); int end = 0; int level = 0; for (int i = start; i < aString.length(); i++) { if (aString.charAt(i) == '(') { level++; } else if (aString.charAt(i) == ')') { level--; if (level == 0) { end = i; break; } } } /* * int x = CoreUtility.innerMostStringStart(aString); int y = * CoreUtility.innerMostStringEnd(aString); // bounds checking if * ((start > end) || (start >= aString.length())) { break; } if * ((end <= 0) || (end >= aString.length())) { break; } */ final String inCalc = aString.substring(start + 1, end); String replacement = "0"; final Float fVal = getVariableValue(aSpell, inCalc, aSpellClass); if (!CoreUtility.doublesEqual(fVal.floatValue(), 0.0f) || (inCalc.contains("MIN")) || (inCalc.contains("MAX")) || inCalc.toUpperCase() .contains("MIN(") || inCalc.toUpperCase() .contains("MAX(")) { found = true; replacement = String.valueOf(fVal.intValue()); } if (found) { aString = aString.substring(0, start) + replacement + aString.substring(end + 1); } else { aString = aString.substring(0, start) + "[" + inCalc + "]" + aString.substring(end + 1); } } return aString; } /** * Removes a "temporary" bonus * * @param aBonus */ public void removeTempBonus(final BonusObj aBonus) { bonusManager.removeTempBonus(aBonus); setDirty(true); } public void removeTempBonusItemList(final Equipment aEq) { tempBonusItemList.remove(aEq); setDirty(true); } public void removeTemplate(final PCTemplate inTmpl) { templateInputFacet.remove(id, inTmpl); setDirty(true); } private static String replaceMasterString(String aString, final int aNum) { while (true) { final int x = aString.indexOf("MASTER"); if (x == -1) { break; } final String leftString = aString.substring(0, x); final String rightString = aString.substring(x + 6); aString = leftString + Integer.toString(aNum) + rightString; } return aString; } public PCLevelInfo addLevelInfo(final String classKeyName) { final PCLevelInfo li = new PCLevelInfo(classKeyName); addLevelInfo(li); return li; } public void addLevelInfo(final PCLevelInfo pli) { levelInfoFacet.add(id, pli); } public void saveStatIncrease(final PCStat stat, final int mod, final boolean isPreMod) { final int idx = getLevelInfoSize() - 1; if (idx >= 0) { levelInfoFacet.get(id, idx).addModifiedStat(stat, mod, isPreMod); } setDirty(true); } public int getStatIncrease(final PCStat stat, final boolean includePost) { final int idx = getLevelInfoSize() - 1; if (idx >= 0) { return levelInfoFacet.get(id, idx).getTotalStatMod(stat, includePost); } return 0; } public int sizeInt() { return sizeFacet.sizeInt(id); } private int totalHitDice() { return levelFacet.getMonsterLevelCount(id); } public int totalNonMonsterLevels() { return levelFacet.getNonMonsterLevelCount(id); } public BigDecimal totalValue() { BigDecimal totalValue = BigDecimal.ZERO; for (Equipment eq : getEquipmentMasterList()) { totalValue = totalValue.add(eq.getCost(this).multiply(BigDecimal.valueOf(eq.qty()))); } return totalValue; } /** * @return true if character is currently being read from file. */ public boolean isImporting() { return importing; } public void giveClassesAway(final PCClass toClass, final PCClass fromClass, int iCount) { if ((toClass == null) || (fromClass == null)) { return; } // Will take destination class over maximum? if (toClass.hasMaxLevel() && (getLevel(toClass) + iCount) > toClass.getSafe(IntegerKey.LEVEL_LIMIT)) { iCount = toClass.getSafe(IntegerKey.LEVEL_LIMIT) - getLevel(toClass); } // Enough levels to move? if ((getLevel(fromClass) <= iCount) || (iCount < 1)) { return; } final int fromLevel = getLevel(fromClass); final int iFromLevel = fromLevel - iCount; final int toLevel = getLevel(toClass); //Capture necessary information Integer[] hpArray = new Integer[iCount+toLevel]; for (int i = 0; i < iCount; i++) { PCClassLevel frompcl = getActiveClassLevel(fromClass, i+iFromLevel); hpArray[i] = getHP(frompcl); } for (int i = 0; i < toLevel; i++) { PCClassLevel topcl = getActiveClassLevel(toClass, i); hpArray[i+iCount] = getHP(topcl); } for (int i = 0; i < iCount; i++) { fromClass.doMinusLevelMods(this, fromLevel - i); } //Do the class level swap fromClass.setLevel(iFromLevel, this); toClass.setLevel(toLevel + iCount, this); //Restore capture info to new class for (int i = 0; i < iCount+toLevel; i++) { PCClassLevel topcl = getActiveClassLevel(toClass, i); setHP(topcl, hpArray[i]); } // first, change the toClass current PCLevelInfo level for (PCLevelInfo pcl : getLevelInfo()) { if (pcl.getClassKeyName().equals(toClass.getKeyName())) { final int iTo = (pcl.getClassLevel() + getLevel(toClass)) - toLevel; pcl.setClassLevel(iTo); } } // change old class PCLevelInfo to the new class for (PCLevelInfo pcl : getLevelInfo()) { if (pcl.getClassKeyName().equals(fromClass.getKeyName()) && (pcl.getClassLevel() > iFromLevel)) { final int iFrom = pcl.getClassLevel() - iFromLevel; pcl.setClassKeyName(toClass.getKeyName()); pcl.setClassLevel(iFrom); } } /* * // get skills associated with old class and link to new class for * (Iterator e = getSkillList().iterator(); e.hasNext();) { Skill aSkill = * (Skill) e.next(); aSkill.replaceClassRank(fromClass.getName(), * toClass.getName()); } toClass.setSkillPool(fromClass.getSkillPool()); */ } public void addFreeLanguage(final Language aLang, CDOMObject source) { freeLangFacet.add(id, aLang, source); setDirty(true); } public void addAddLanguage(final Language aLang, CDOMObject source) { addLangFacet.add(id, aLang, source); setDirty(true); } public void removeAddLanguage(final Language aLang, CDOMObject source) { addLangFacet.remove(id, aLang, source); setDirty(true); } public void addAutoLanguage(Language l, Object obj) { autoLangListFacet.add(id, l, obj); } public void removeAutoLanguage(Language l, Object obj) { autoLangListFacet.remove(id, l, obj); } /** * Scan through the list of domains the character has to ensure that they * are all still valid. Any invalid domains will be removed from the * character. */ public void validateCharacterDomains() { //Clone to avoid Concurrent Mod Exception, CODE-153 for (Domain d : new ArrayList<>(getDomainSet())) { if (!isDomainValid(d, this.getDomainSource(d))) { removeDomain(d); } } } private boolean isDomainValid(Domain domain, ClassSource cs) { if (domain == null) { return false; } final PCClass aClass = getClassKeyed(cs.getPcclass().getKeyName()); return ((aClass != null) && (getLevel(aClass) >= cs.getLevel())); } /** * Active BonusObjs * * @return List */ public Collection<BonusObj> getActiveBonusList() { return bonusManager.getActiveBonusList(); } /** * Parses through all Equipment items and calculates total Bonus * * @param aType * @param aName * @return equipment bonus to */ private double getEquipmentBonusTo(String aType, String aName) { double bonus = 0; if (!hasEquipment()) { return bonus; } aType = aType.toUpperCase(); aName = aName.toUpperCase(); for (Equipment eq : getEquippedEquipmentSet()) { final List<BonusObj> tempList = eq.getBonusListOfType(this, aType, aName, true); if (eq.isWeapon() && eq.isDouble()) { tempList.addAll(eq.getBonusListOfType(this, aType, aName, false)); } bonus += calcBonusFromList(tempList, eq); } return bonus; } public int getNumAttacks() { return Math.min(Math.max(baseAttackBonus() / 5, 4), 1); } /** * Returns a bonus. * * @param aList * @param aType * @param aName * @return double */ private double getPObjectWithCostBonusTo(final Collection<? extends CDOMObject> aList, final String aType, final String aName) { double iBonus = 0; if (aList.isEmpty()) { return iBonus; } for (CDOMObject anObj : aList) { final List<BonusObj> tempList = BonusUtilities.getBonusFromList(anObj.getBonusList(this), aType, aName); iBonus += calcBonusWithCostFromList(tempList); } return iBonus; } /** * Get the class level as a String * * @param aClassKey * @param doReplace * @return class level as String */ public String getClassLevelString(String aClassKey, final boolean doReplace) { int lvl = 0; int idx = aClassKey.indexOf(";BEFORELEVEL="); if (idx < 0) { idx = aClassKey.indexOf(";BEFORELEVEL."); } if (idx > 0) { lvl = Integer.parseInt(aClassKey.substring(idx + 13)); aClassKey = aClassKey.substring(0, idx); } if (doReplace) { aClassKey = aClassKey.replace('{', '(').replace('}', ')'); } if (aClassKey.startsWith("TYPE=") || aClassKey.startsWith("TYPE.")) { int totalLevels = 0; String[] classTypes = aClassKey.substring(5).split("\\."); CLASSFOR: for (PCClass cl : getClassSet()) { for (String type : classTypes) { if (!cl.isType(type)) { continue CLASSFOR; } if (lvl > 0) { totalLevels += getLevelBefore(cl.getKeyName(), lvl); } totalLevels += getLevel(cl); } } return Integer.toString(totalLevels); } else { final PCClass aClass = getClassKeyed(aClassKey); if (aClass != null) { if (lvl > 0) { return Integer.toString(getLevelBefore(aClass.getKeyName(), lvl)); } return Integer.toString(getLevel(aClass)); } return "0"; } } public int getLevelBefore(final String classKey, final int charLevel) { String thisClassKey; int lvl = 0; for (int idx = 0; idx < charLevel; ++idx) { thisClassKey = getLevelInfoClassKeyName(idx); if (thisClassKey.isEmpty()) { break; } if (thisClassKey.equals(classKey)) { ++lvl; } } return lvl; } public List<? extends CDOMObject> getCDOMObjectList() { List<CDOMObject> list = new ArrayList<>(expandedCampaignFacet.getSet(id)); // Loaded campaigns // Alignment PCAlignment align = alignmentFacet.get(id); if (align != null) { list.add(align); } // BioSet list.add(bioSetFacet.get(id)); list.addAll(checkFacet.getSet(id)); // Class list.addAll(classFacet.getSet(id)); // CompanionMod list.addAll(companionModFacet.getSet(id)); // Deity Deity deity = deityFacet.get(id); if (deity != null) { list.add(deity); } // Domain list.addAll(domainFacet.getSet(id)); // Equipment for (Equipment eq : activeEquipmentFacet.getSet(id)) { list.add(eq); for (EquipmentModifier eqMod : eq.getEqModifierList(true)) { list.add(eqMod); } for (EquipmentModifier eqMod : eq.getEqModifierList(false)) { list.add(eqMod); } } // Feats and abilities (virtual feats, auto feats) for (AbilityCategory cat : SettingsHandler.getGame().getAllAbilityCategories()) { list.addAll(getAggregateAbilityListNoDuplicates(cat)); } // Race Race race = raceFacet.get(id); if (race != null) { list.add(race); } // SizeAdjustment SizeAdjustment sa = sizeFacet.get(id); if (sa != null) { list.add(sa); } // Skill list.addAll(skillFacet.getSet(id)); // Stat (PCStat) list.addAll(statFacet.getSet(id)); // Template (PCTemplate) list.addAll(templateFacet.getSet(id)); for (PCClass cl : getClassSet()) { for (int i = 1; i <= getLevel(cl); i++) { PCClassLevel classLevel = getActiveClassLevel(cl, i); list.add(classLevel); } } return list; } /** * availableSpells sk4p 13 Dec 2002 * * For learning or preparing a spell: Are there slots available at this * level or higher Fixes BUG [569517] * * @param level * the level being checked for availability * @param aClass * the class under consideration * @param bookName * the name of the spellbook * @param knownLearned * "true" if this is learning a spell, "false" if prepping * @param isSpecialtySpell * "true" if this is a speciality for the given class * @return true or false, a new spell can be added */ public boolean availableSpells(final int level, final PCClass aClass, final String bookName, final boolean knownLearned, final boolean isSpecialtySpell) { boolean available = false; FactKey<String> fk = FactKey.valueOf("SpellType"); String spelltype = aClass.getResolved(fk); final boolean isDivine = ("Divine".equalsIgnoreCase(spelltype)); final boolean canUseHigher = knownLearned ? useHigherKnownSlots : useHigherPreppedSlots; int knownTot; int knownNon; int knownSpec; int memTot; int memNon; int memSpec; // int excTot int excNon; // int excTot int excSpec; int lowExcSpec = 0; int lowExcNon = 0; int goodExcSpec = 0; int goodExcNon = 0; for (int i = 0; i < level; ++i) { // Get the number of castable slots if (knownLearned) { knownNon = this.getSpellSupport(aClass).getKnownForLevel(i, this); knownSpec = this.getSpellSupport(aClass).getSpecialtyKnownForLevel(i, this); knownTot = knownNon + knownSpec; // TODO: : value never used } else { // Get the number of castable slots knownTot = this.getSpellSupport(aClass).getCastForLevel(i, bookName, true, true, this); knownNon = this.getSpellSupport(aClass).getCastForLevel(i, bookName, false, true, this); knownSpec = knownTot - knownNon; } // Now get the number of spells memorised, total and specialities memTot = SpellCountCalc.memorizedSpellForLevelBook(this, aClass, i, bookName); memSpec = SpellCountCalc.memorizedSpecialtiesForLevelBook(i, bookName, this, aClass); memNon = memTot - memSpec; // Excess castings excSpec = knownSpec - memSpec; excNon = knownNon - memNon; // Now we spend these slots making up any deficits in lower levels // while ((excNon > 0) && (lowExcNon < 0)) { --excNon; ++lowExcNon; } while ((excSpec > 0) && (lowExcSpec < 0)) { --excSpec; ++lowExcSpec; } if (!isDivine || knownLearned) { // If I'm not divine, I can use non-specialty slots of this // level // to take up the slack of my excess speciality spells from // lower levels. while ((excNon > 0) && (lowExcSpec < 0)) { --excNon; ++lowExcSpec; } // And I can use non-specialty slots of this level to take // up the slack of my excess speciality spells of this level. // while ((excNon > 0) && (excSpec < 0)) { --excNon; ++excSpec; } } // Now, if there are slots left over, I don't add them to the // running totals. // Spell slots of this level won't help me at the next level. // Deficits, however, will have to be made up at the next level. // if (excSpec < 0) { lowExcSpec += excSpec; } if (excNon < 0) { lowExcNon += excNon; } } for (int i = level; i <= Constants.MAX_SPELL_LEVEL; ++i) { if (knownLearned) { knownNon = this.getSpellSupport(aClass).getKnownForLevel(i, this); knownSpec = this.getSpellSupport(aClass).getSpecialtyKnownForLevel(i, this); knownTot = knownNon + knownSpec; // for completeness } else { // Get the number of castable slots knownTot = this.getSpellSupport(aClass).getCastForLevel(i, bookName, true, true, this); knownNon = this.getSpellSupport(aClass).getCastForLevel(i, bookName, false, true, this); knownSpec = knownTot - knownNon; } // At the level currently being looped through, if the number of // casts // is zero, that means we have reached a level beyond which no // higher-level // casts are possible. Therefore, it's time to break. // Likewise if we aren't allowed to use higher level slots, no sense // in // going higher than the spell's level. // if (!canUseHigher && i > level) { break; } // Now get the number of spells memorised, total and specialities memTot = SpellCountCalc.memorizedSpellForLevelBook(this, aClass, i, bookName); memSpec = SpellCountCalc.memorizedSpecialtiesForLevelBook(i, bookName, this, aClass); memNon = memTot - memSpec; // Excess castings excSpec = knownSpec - memSpec; excNon = knownNon - memNon; // Now we spend these slots making up any deficits in lower levels // while ((excNon > 0) && (lowExcNon < 0)) { --excNon; ++lowExcNon; } while ((excNon > 0) && (goodExcNon < 0)) { --excNon; ++goodExcNon; } while ((excSpec > 0) && (lowExcSpec < 0)) { --excSpec; ++lowExcSpec; } while ((excSpec > 0) && (goodExcSpec < 0)) { --excSpec; ++goodExcSpec; } if (!isDivine) { // If I'm not divine, I can use non-specialty slots of this // level // to take up the slack of my excess speciality spells from // lower levels. while ((excNon > 0) && (lowExcSpec < 0)) { --excNon; ++lowExcSpec; } // And also for levels sufficiently high for the spell that got // me // into this mess, but of lower level than the level currently // being calculated. while ((excNon > 0) && (goodExcSpec < 0)) { --excNon; ++goodExcSpec; } // And finally use non-specialty slots of this level to take // up the slack of excess speciality spells of this level. // while ((excNon > 0) && (excSpec < 0)) { --excNon; ++excSpec; } } // Right now, if there are slots left over at this level, // it means that there are slots left to add the spell that started // all of this. if (!isSpecialtySpell && (excNon > 0) && (excNon + excSpec > 0)) { available = true; } // Account for specialty spells using up non specialty slots if (isDivine) { if (isSpecialtySpell && (excSpec > 0)) { available = true; } } else if (isSpecialtySpell && (excNon + excSpec > 0)) { available = true; } // If we found a slot, we need look no further. if (available) { break; } // Now, if there are slots left over, I don't add them to the // running totals. // Spell slots of this level won't help me at the next level. // Deficits, however, will have to be made up at the next level. // if (excSpec < 0) { goodExcSpec += excSpec; } if (excNon < 0) { goodExcNon += excNon; } } return available; } /** * Compute total bonus from a List of BonusObjs Use cost of bonus to adjust * total bonus up or down This method takes a list of bonus objects. * * For each object in the list, it gets the creating object and queries it * for its "COST". It then multiplies the value of the bonus by this cost * and adds it to the cumulative total so far. If subSearch is true, the * choices made in the object that the bonus originated in are searched, the * effective bonus is multiplied by the number of times this bonus appears * in the list. * * Note: This COST seems to be used for several different things in the code * base, in feats for instance, it is used to modify the feat pool by * amounts other than 1 when selecting a given feat. Here it is used as a * multiplier to say how effective a given bonus is i.e. a bonus with a COST * of 0.5 counts for half its normal value. The COST is limited to a max of * 1, so it can only make bonuses less effective. * * @param aList * a list of bonus objects * @return the calculated cumulative bonus */ private double calcBonusWithCostFromList(final List<BonusObj> aList) { return bonusManager.calcBonusesWithCost(aList); } /** * calculate the total racial modifier to save: racial bonuses like the * standard halfling's +1 on all saves template bonuses like the Lightfoot * halfling's +1 on all saves racial base modifiers for certain monsters * * @param check * @return int */ private int calculateSaveBonusRace(PCCheck check) { final String sString = check.toString(); Race race = getRace(); int save = (int) BonusCalc.charBonusTo(race, "SAVE", "BASE." + sString, this); save += (int) BonusCalc.charBonusTo(race, "SAVE", sString, this); return save; } /** * Counts the number of spells inside a spellbook Yes, divine casters can * have a "spellbook" * * @param aString * @return spells in a book */ public int countSpellsInBook(final String aString) { final StringTokenizer aTok = new StringTokenizer(aString, "."); final int classNum = Integer.parseInt(aTok.nextToken()); final int sbookNum = Integer.parseInt(aTok.nextToken()); final int levelNum; if (sbookNum >= getSpellBookCount()) { return 0; } if (aTok.hasMoreTokens()) { levelNum = Integer.parseInt(aTok.nextToken()); } else { levelNum = -1; } String bookName = Globals.getDefaultSpellBook(); if (sbookNum > 0) { bookName = getSpellBookNames().get(sbookNum); } final PObject aObject = getSpellClassAtIndex(classNum); if (aObject != null) { final List<CharacterSpell> aList = getCharacterSpells(aObject, null, bookName, levelNum); return aList.size(); } return 0; } public SizeAdjustment getSizeAdjustment() { return sizeFacet.get(id); } public int getSpellClassCount() { return getSpellClassList().size(); } /** * Get the spell class list * * @return List */ public List<? extends PObject> getSpellClassList() { final List<PObject> aList = new ArrayList<>(); Race race = getRace(); if (!getCharacterSpells(race).isEmpty()) { aList.add(race); } for (PCClass pcClass : getClassSet()) { if (pcClass.get(FactKey.valueOf("SpellType")) != null) { aList.add(pcClass); } } return aList; } public boolean includeSkill(final Skill skill, final SkillFilter filter) { if (skill.getSafe(ObjectKey.EXCLUSIVE) && !this.isClassSkill(skill) && !this.isCrossClassSkill(skill)) { return false; } switch (filter) { case Ranks: return (SkillRankControl.getTotalRank(this, skill) .floatValue() > 0); case NonDefault: return (SkillRankControl.getTotalRank(this, skill) .floatValue() > 0 || SkillModifier.modifier(skill, this) != SkillModifier.getStatMod(skill, this) + getSizeAdjustmentBonusTo("SKILL", skill.getKeyName())); case Usable: return qualifySkill(skill) && (SkillRankControl.getTotalRank(this, skill) .floatValue() > 0 || skill .getSafe(ObjectKey.USE_UNTRAINED)); default: return qualifySkill(skill); } } private boolean qualifySkill(final Skill skill) { return skill.qualifies(this, skill); } /** * Change the number of levels a character has in a particular class. Note: * It is assumed that this method is not used as part of loading a * previously saved character. there is no way to bypass the prerequisites * with this method, see: incrementClassLevel(int, PCClass, boolean, * boolean); * * * @param numberOfLevels * number of levels to add * @param globalClass * the class to add the levels to * @param bSilent * whether or not to display warning messages */ public void incrementClassLevel(final int numberOfLevels, final PCClass globalClass, final boolean bSilent) { incrementClassLevel(numberOfLevels, globalClass, bSilent, false); } /** * Change the number of levels a character has in a particular class. Note: * It is assumed that this method is not used as part of loading a * previously saved character. * * @param numberOfLevels * The number of levels to add or remove. If a positive number is * passed in then that many levels will be added. If the number * of levels passed in is negative then that many levels will be * removed from the specified class. * @param globalClass * The global class from the data store. The class as stored in * the character will be compared to this one using the * getClassNamed() method * @param bSilent * If true do not display any warning messages about adding or * removing too many levels * @param bypassPrereqs * Whether we should bypass the checks as to whether or not the * PC qualifies to take this class. If true, the checks will be * bypassed */ public void incrementClassLevel(final int numberOfLevels, final PCClass globalClass, final boolean bSilent, final boolean bypassPrereqs) { // If not importing, load the spell list if (!importing) { getSpellList(); } // Make sure the character qualifies for the class if adding it if (numberOfLevels > 0) { if (!bypassPrereqs && !globalClass.qualifies(this, globalClass)) { return; } Race race = getRace(); if (globalClass.isMonster() && !SettingsHandler.isIgnoreMonsterHDCap() && !race.isAdvancementUnlimited() && ((totalHitDice() + numberOfLevels) > race.maxHitDiceAdvancement()) && !bSilent) { ShowMessageDelegate.showMessageDialog("Cannot increase Monster Hit Dice for this character beyond " + race.maxHitDiceAdvancement() + ". This character's current number of Monster Hit Dice is " + totalHitDice(), Constants.APPLICATION_NAME, MessageType.INFORMATION); return; } } // Check if the character already has the class. PCClass pcClassClone = getClassKeyed(globalClass.getKeyName()); // If the character did not already have the class... if (pcClassClone == null) { // add the class even if setting to level 0 if (numberOfLevels >= 0) { // Get a clone of the class so we don't modify the globals! pcClassClone = globalClass.clone(); //Still required :( // Make sure the clone was successful if (pcClassClone == null) { Logging.errorPrint("PlayerCharacter::incrementClassLevel => " + "Clone of class " + globalClass.getKeyName() + " failed!"); return; } // If not importing, add extra feats if (!importing && classFacet.isEmpty(id)) { adjustAbilities(AbilityCategory.FEAT, new BigDecimal( pcClassClone.getSafe(IntegerKey.START_FEATS))); } // Add the class to the character classes as level 0 classFacet.addClass(id, pcClassClone); } else { // mod is < 0 and character does not have class. Return. return; } } // Add or remove levels as needed if (numberOfLevels > 0) { for (int i = 0; i < numberOfLevels; ++i) { int currentLevel = getLevel(pcClassClone); final PCLevelInfo playerCharacterLevelInfo = addLevelInfo(pcClassClone.getKeyName()); // if we fail to add the level, remove and return if (!pcClassClone.addLevel(false, bSilent, this, bypassPrereqs)) { PCClassLevel failedpcl = getActiveClassLevel(pcClassClone, currentLevel + 1); removeLevelInfo(pcClassClone.getKeyName()); return; } } } else if (numberOfLevels < 0) { for (int i = 0; i < -numberOfLevels; ++i) { int currentLevel = getLevel(pcClassClone); pcClassClone.subLevel(this); PCLevelInfo removedLI = removeLevelInfo(pcClassClone.getKeyName()); int pointsToRemove = removedLI.getSkillPointsGained(this) - removedLI.getSkillPointsRemaining(); SkillRankControl.removeSkillsForTopLevel(this, pcClassClone, currentLevel, pointsToRemove); } } calcActiveBonuses(); } /** * Remove from the character the PCLevelInfo representing the highest level * of the supplied class. * * @param classKeyName The keyname of the class to have a level removed. * @return The level removed, or null if none was found */ private PCLevelInfo removeLevelInfo(final String classKeyName) { for (int idx = getLevelInfoSize() - 1; idx >= 0; --idx) { final PCLevelInfo li = levelInfoFacet.get(id, idx); if (li.getClassKeyName().equals(classKeyName)) { levelInfoFacet.remove(id, li); setDirty(true); return li; } } return null; } /** * {@code rollStats} roll Globals.s_ATTRIBLONG.length random stats * Method: 1: 4d6 Drop Lowest 2: 3d6 3: 5d6 Drop 2 Lowest 4: 4d6 reroll 1's * drop lowest 5: 4d6 reroll 1's and 2's drop lowest 6: 3d6 +5 7: 5d6 Drop * lowest and middle as per FREQ #458917 * * @param method * the method to be used for rolling. */ public void rollStats(final int method) { int aMethod = method; if (SettingsHandler.getGame().isPurchaseStatMode()) { aMethod = Constants.CHARACTER_STAT_METHOD_PURCHASE; } rollStats(aMethod, statFacet.getSet(id), SettingsHandler.getGame().getCurrentRollingMethod(), false); } public void rollStats(final int method, final Collection<PCStat> aStatList, final RollMethod rollMethod, boolean aSortedFlag) { int[] rolls = new int[aStatList.size()]; for (int i = 0; i < rolls.length; i++) { switch (method) { case Constants.CHARACTER_STAT_METHOD_PURCHASE: rolls[i] = SettingsHandler.getGame().getPurchaseModeBaseStatScore(this); break; case Constants.CHARACTER_STAT_METHOD_ALL_THE_SAME: rolls[i] = SettingsHandler.getGame().getAllStatsValue(); break; case Constants.CHARACTER_STAT_METHOD_ROLLED: final String diceExpression = rollMethod.getMethodRoll(); rolls[i] = RollingMethods.roll(diceExpression); break; default: rolls[i] = 0; break; } } if (aSortedFlag) { Arrays.sort(rolls); } int i = rolls.length - 1; for (PCStat currentStat : aStatList) { setStat(currentStat, 0); if (!currentStat.getSafe(ObjectKey.ROLLED)) { continue; } int roll = rolls[i--] + getStat(currentStat); if (roll < currentStat.getSafe(IntegerKey.MIN_VALUE)) { roll = currentStat.getSafe(IntegerKey.MIN_VALUE); } if (roll > currentStat.getSafe(IntegerKey.MAX_VALUE)) { roll = currentStat.getSafe(IntegerKey.MAX_VALUE); } setStat(currentStat, roll); } if (method != Constants.CHARACTER_STAT_METHOD_PURCHASE) { poolAmount = 0; this.costPool = 0; } if (method != Constants.CHARACTER_STAT_METHOD_PURCHASE) { poolAmount = 0; } } /** * Sorts the provided list of equipment in output order. This is in * ascending order of the equipment's outputIndex field. If multiple items * of equipment have the same outputIndex they will be ordered by name. Note * hidden items (outputIndex = -1) are not included in list. * * @param unsortedEquip * An ArrayList of the equipment to be sorted. * @param merge * How to merge. * @return An ArrayList of the equipment objects in output order. */ private static List<Equipment> sortEquipmentList(final Collection<Equipment> unsortedEquip, final int merge) { if (unsortedEquip.isEmpty()) { // Create a real list so it can be added to later on return new ArrayList<>(); } // Merge list for duplicates // The sorting is done during the Merge final List<Equipment> sortedList = CoreUtility.mergeEquipmentList(unsortedEquip, merge); // Remove the hidden items from the list for (Iterator<Equipment> i = sortedList.iterator(); i.hasNext();) { final Equipment item = i.next(); if (item.getOutputIndex() == -1) { i.remove(); } } return sortedList; } /** * @param descriptionLst * The descriptionLst to set. */ private void setDescriptionLst(final String descriptionLst) { this.descriptionLst = descriptionLst; } /** * Prepares this PC object for output by ensuring that all its * info is up to date. */ public void preparePCForOutput() { // Get the EquipSet used for output and calculations // possibly include equipment from temporary bonuses setCalcEquipmentList(useTempMods); // Make sure spell lists are setup getSpellList(); // Calculate any active bonuses calcActiveBonuses(); //Sort Skills SkillDisplay.resortSelected(this, skillsOutputOrder); // Determine which hands weapons are currently being wielded in determinePrimaryOffWeapon(); // Recalculate the movement rates adjustMoveRates(); // Calculate any active bonuses calcActiveBonuses(); } private static final class CasterLevelSpellBonus { private int bonus; private String type; /** * Constructor * * @param b * @param t */ private CasterLevelSpellBonus(final int b, final String t) { bonus = b; type = t; } /** * Get bonus * * @return bonus */ public int getBonus() { return bonus; } /** * Get type * * @return type */ public String getType() { return type; } /** * Set bonus * * @param newBonus */ public void setBonus(final int newBonus) { bonus = newBonus; } @Override public String toString() { return ("bonus: " + bonus + " type: " + type); } } /** * @param info * @return character level */ public int getCharacterLevel(final PCLevelInfo info) { int i = 1; for (PCLevelInfo element : getLevelInfo()) { if (info == element) { return i; } i++; } return -1; } /** * Return a list of bonus languages which the character may select from. * This function is not efficient, but is sufficient for it's current use of * only being called when the user requests the bonus language selection * list. Note: A check will be made for the ALL language and it will be * replaced with the current list of languages in globals. These should be * further restricted by the prerequisites of the languages to ensure that * 'secret' languages are not offered. * * @return List of bonus languages for the character. */ public Set<Language> getLanguageBonusSelectionList() { return startingLangFacet.getSet(id); } /** * Retrieve the value of the stat excluding either temporary bonuses, * equipment bonuses or both. This method ensure stacking rules are applied * to all included bonuses. If not excluding either, it is quicker to use * getTotalBonusTo. * * @param stat * The stat to calculate the bonus for. * @param useTemp * Should temp bonuses be included? * @param useEquip * Should equipment bonuses be included? * @return The bonus to the stat. */ public int getPartialStatFor(PCStat stat, boolean useTemp, boolean useEquip) { int partialStatBonus = bonusManager.getPartialStatBonusFor(stat, useTemp, useEquip); return statCalcFacet.getPartialStatFor(id, stat, partialStatBonus); } /** * Retrieve the stat as it was at a particular level excluding either * temporary bonuses, equipment bonuses or both. This method ensures * stacking rules are applied to all included bonuses. If not excluding * either, it is quicker to use getTotalStatAtLevel. * * @param stat * The stat to calculate the value of. * @param level * The level we want to see the stat at. * @param usePost * Should stat mods that occurred after levelling be included? * @param useTemp * Should temp bonuses be included? * @param useEquip * Should equipment bonuses be included? * @return The stat as it was at the level */ public int getPartialStatAtLevel(PCStat stat, int level, boolean usePost, boolean useTemp, boolean useEquip) { int curStat = StatAnalysis.getPartialStatFor(this, stat, useTemp, useEquip); for (int idx = getLevelInfoSize() - 1; idx >= level; --idx) { final int statLvlAdjust = levelInfoFacet.get(id, idx).getTotalStatMod(stat, usePost); curStat -= statLvlAdjust; } return curStat; } /** * Returns a deep copy of the PlayerCharacter. Note: This method does a * shallow copy of many lists in here that seem to point to "system" * objects. These copies should be validated before using this method. * * @return a new deep copy of the {@code PlayerCharacter} */ @Override public PlayerCharacter clone() { PlayerCharacter aClone = null; // calling super.clone won't work because it will not create // new data instances for all the final variables and I won't // be able to reset them. Need to call new PlayerCharacter() // aClone = (PlayerCharacter)super.clone(); aClone = new PlayerCharacter(campaignFacet.getSet(id)); //aClone.variableProcessor = new VariableProcessorPC(aClone); try { aClone.assocSupt = assocSupt.clone(); } catch (CloneNotSupportedException e) { Logging.errorPrint("PlayerCharacter.clone failed", e); } Collection<AbstractStorageFacet> beans = SpringHelper.getStorageBeans(); for (AbstractStorageFacet bean : beans) { bean.copyContents(id, aClone.id); } aClone.bonusManager = bonusManager.buildDeepClone(aClone); for (PCClass cloneClass : aClone.classFacet.getSet(aClone.id)) { cloneClass.addFeatPoolBonus(aClone); } Follower followerMaster = masterFacet.get(id); if (followerMaster != null) { aClone.masterFacet.set(id, followerMaster.clone()); } else { aClone.masterFacet.remove(id); } aClone.equipSetFacet.removeAll(aClone.id); for (EquipSet eqSet : equipSetFacet.getSet(id)) { aClone.addEquipSet((EquipSet) eqSet.clone()); } List<Equipment> equipmentMasterList = aClone.getEquipmentMasterList(); aClone.userEquipmentFacet.removeAll(aClone.id); aClone.equipmentFacet.removeAll(aClone.id); aClone.equippedFacet.removeAll(aClone.id); FacetLibrary.getFacet(SourcedEquipmentFacet.class).removeAll(aClone.id); for (Equipment equip : equipmentMasterList) { aClone.addEquipment(equip.clone()); } aClone.levelInfoFacet.removeAll(aClone.id); for (PCLevelInfo info : getLevelInfo()) { PCLevelInfo newLvlInfo = info.clone(); aClone.levelInfoFacet.add(aClone.id, newLvlInfo); } aClone.spellBookFacet.removeAll(aClone.id); for (String book : spellBookFacet.getBookNames(id)) { aClone.addSpellBook((SpellBook) spellBookFacet.getBookNamed(id, book).clone()); } aClone.calcEquipSetId = calcEquipSetId; aClone.tempBonusItemList.addAll(tempBonusItemList); aClone.descriptionLst = descriptionLst; aClone.autoKnownSpells = autoKnownSpells; aClone.autoLoadCompanion = autoLoadCompanion; aClone.autoSortGear = autoSortGear; aClone.outputSheetHTML = outputSheetHTML; aClone.outputSheetPDF = outputSheetPDF; aClone.ageSetKitSelections = new boolean[10]; aClone.defaultDomainSource = defaultDomainSource; System.arraycopy(ageSetKitSelections, 0, aClone.ageSetKitSelections, 0, ageSetKitSelections.length); // Not sure what this is for aClone.importing = false; aClone.useTempMods = useTempMods; aClone.costPool = costPool; aClone.currentEquipSetNumber = currentEquipSetNumber; aClone.poolAmount = poolAmount; // order in which the skills will be output. aClone.skillsOutputOrder = skillsOutputOrder; aClone.spellLevelTemp = spellLevelTemp; aClone.pointBuyPoints = pointBuyPoints; aClone.adjustMoveRates(); //This mod set is necessary to trigger certain calculations to ensure correct output //modSkillPointsBuffer = Integer.MIN_VALUE; aClone.calcActiveBonuses(); //Just to be safe aClone.equippedFacet.reset(aClone.id); aClone.serial = serial; return aClone; } /** * Set the string for the characteristic * * @param key * @param s */ public void setStringFor(PCStringKey key, String s) { assert(key != null); String currValue = factFacet.get(id, key); if (PlayerCharacter.shouldDirtyForChange(s, currValue)) { factFacet.set(id, key, s); setDirty(true); } } private static boolean shouldDirtyForChange(final String s, final String currValue) { return (currValue == null && s != null) || (currValue != null && !currValue.equals(s)); } private Float getEquippedQty(EquipSet eSet, Equipment eqI) { return equipSetFacet.getEquippedQuantity(id, eSet, eqI); } /** * If an item can only go in one location, return the name of that location * to add to an EquipSet * * @param eqI * @return single location */ private String getSingleLocation(Equipment eqI) { // Handle natural weapons String loc = getNaturalWeaponLocation(eqI); if (loc != null) { return loc; } // Always force weapons to go through the chooser dialog // unless they are also armor (ie: with Armor Spikes) if ((eqI.isWeapon()) && !(eqI.isArmor())) { return Constants.EMPTY_STRING; } List<EquipSlot> eqSlotList = SystemCollections.getUnmodifiableEquipSlotList(); if ((eqSlotList == null) || eqSlotList.isEmpty()) { return Constants.EMPTY_STRING; } for (EquipSlot es : eqSlotList) { // see if this EquipSlot can contain this item TYPE if (es.canContainType(eqI.getType())) { return es.getSlotName(); } } return Constants.EMPTY_STRING; } /** * Identify the equipping location for a natural weapon. * @param eqI The natural weapon * @return The location name, or null if not a natural weapon. */ public String getNaturalWeaponLocation(Equipment eqI) { if (eqI.isNatural()) { if (eqI.getSlots(this) == 0) { if (eqI.isPrimaryNaturalWeapon()) { return Constants.EQUIP_LOCATION_NATURAL_PRIMARY; } return Constants.EQUIP_LOCATION_NATURAL_SECONDARY; } } return null; } /** * returns true if you can put Equipment into a location in EquipSet * * @param eSet * @param locName * @param eqI * @param eqTarget * @return true if equipment can be added */ private boolean canEquipItem(EquipSet eSet, String locName, Equipment eqI, Equipment eqTarget) { final String idPath = eSet.getIdPath(); // If target is a container, allow it if ((eqTarget != null) && eqTarget.isContainer()) { // TODO - Should make sure eqI can be contained by eqTarget return true; } // If Carried/Equipped/Not Carried slot // allow as many as they would like if (locName.startsWith(Constants.EQUIP_LOCATION_CARRIED) || locName.startsWith(Constants.EQUIP_LOCATION_EQUIPPED) || locName.startsWith(Constants.EQUIP_LOCATION_NOTCARRIED)) { return true; } // allow as many unarmed items as you'd like if (eqI.isUnarmed()) { return true; } // allow many Secondary Natural weapons if (locName.equals(Constants.EQUIP_LOCATION_NATURAL_SECONDARY)) { return true; } // Don't allow weapons that are too large for PC if (eqI.isWeapon() && eqI.isWeaponOutsizedForPC(this) && !eqI.isNatural()) { return false; } // make a HashMap to keep track of the number of each // item that is already equipped to a slot Map<String, String> slotMap = new HashMap<>(); for (EquipSet es : getEquipSet()) { String esID = es.getParentIdPath() + Constants.EQUIP_SET_PATH_SEPARATOR; String abID = idPath + Constants.EQUIP_SET_PATH_SEPARATOR; if (!esID.startsWith(abID)) { continue; } // check to see if we already have // an item in that particular location if (es.getName().equals(locName)) { final Equipment eItem = es.getItem(); final String nString = slotMap.get(locName); int existNum = 0; if (nString != null) { existNum = Integer.parseInt(nString); } if (eItem != null) { existNum += eItem.getSlots(this); } slotMap.put(locName, String.valueOf(existNum)); } } for (EquipSet es : getEquipSet()) { String esID = es.getParentIdPath() + Constants.EQUIP_SET_PATH_SEPARATOR; String abID = idPath + Constants.EQUIP_SET_PATH_SEPARATOR; if (!esID.startsWith(abID)) { continue; } // if it's a weapon we have to do some // checks for hands already in use if (eqI.isWeapon() && !eqI.isNatural()) { // weapons can never occupy the same slot if (es.getName().equals(locName)) { return false; } // if Double Weapon or Both Hands, then no // other weapon slots can be occupied if ((locName.equals(Constants.EQUIP_LOCATION_BOTH) || locName.equals(Constants.EQUIP_LOCATION_DOUBLE)) && (es.getName().equals(Constants.EQUIP_LOCATION_PRIMARY) || es.getName().equals(Constants.EQUIP_LOCATION_SECONDARY) || es.getName().equals(Constants.EQUIP_LOCATION_BOTH) || es.getName().equals( Constants.EQUIP_LOCATION_DOUBLE))) { return false; } // inverse of above case if ((locName.equals(Constants.EQUIP_LOCATION_PRIMARY) || locName .equals(Constants.EQUIP_LOCATION_SECONDARY)) && (es.getName().equals(Constants.EQUIP_LOCATION_BOTH) || es.getName().equals( Constants.EQUIP_LOCATION_DOUBLE))) { return false; } } // If we already have an item in that location // check to see how many are allowed in that slot if (es.getName().equals(locName)) { final String nString = slotMap.get(locName); int existNum = 0; if (nString != null) { existNum = Integer.parseInt(nString); } existNum += eqI.getSlots(this); EquipSlot eSlot = Globals.getEquipSlotByName(locName); if (eSlot == null) { return true; } for (String slotType : eSlot.getContainType()) { if (eqI.isType(slotType)) { // if the item takes more slots, return false if (existNum > (eSlot.getSlotCount() + (int) getTotalBonusTo("SLOTS", slotType))) { return false; } } } return true; } } return true; } /** * Checks to see if Equipment exists in selected EquipSet and if so, then * return the EquipSet containing eqI * * @param eSet * @param eqI * @return EquipSet */ public EquipSet getEquipSetForItem(EquipSet eSet, Equipment eqI) { final String rPath = eSet.getIdPath(); for (EquipSet es : getEquipSet()) { String esIdPath = es.getIdPath() + Constants.EQUIP_SET_PATH_SEPARATOR; String rIdPath = rPath + Constants.EQUIP_SET_PATH_SEPARATOR; if (!esIdPath.startsWith(rIdPath)) { continue; } if (eqI.getName().equals(es.getValue())) { return es; } } return null; } /** * returns a new id_Path with the last id one higher than the current highest * child of the supplied EquipSet. * * @param eSet The equipset which would be the parent of a new node. * @return new id path */ private String getNewIdPath(EquipSet eSet) { String pid = Constants.EQUIP_SET_ROOT_ID; if (eSet != null) { pid = eSet.getIdPath(); } int newID = getNewChildId(pid); return pid + Constants.EQUIP_SET_PATH_SEPARATOR + newID; } /** * Identify a new id (only the final number in the path) for a child * equipment set. The id is guarantyeed to be unique and have no siblings * with higher ids. * * @param pid The parent path. * @return New id for a child node */ public int getNewChildId(String pid) { int newID = 0; for (EquipSet es : getEquipSet()) { if (es.getParentIdPath().equals(pid) && (es.getId() > newID)) { newID = es.getId(); } } ++newID; return newID; } public EquipSet addEquipToTarget(final EquipSet eSet, final Equipment eqTarget, String locName, final Equipment eqI, Float newQty) { float tempQty = 1.0f; if (newQty != null) { tempQty = newQty.floatValue(); } else { newQty = tempQty; } boolean addAll = false; boolean mergeItem = false; Equipment masterEq = getEquipmentNamed(eqI.getName()); if (masterEq == null) { return null; } float diffQty = masterEq.getQty().floatValue() - getEquippedQty(eSet, eqI).floatValue(); // if newQty is less than zero, we want to // add all of this item to the EquipSet // or all remaining items that havn't already // been added to the EquipSet if (newQty.floatValue() < 0.0f) { tempQty = diffQty; newQty = new Float(tempQty + getEquippedQty(eSet, eqI).floatValue()); addAll = true; } // Check to make sure this EquipSet does not exceed // the PC's equipmentList number for this item if (tempQty > diffQty) { return null; } // check to see if the target item is a container if ((eqTarget != null) && eqTarget.isContainer()) { // set these to newQty just for testing eqI.setQty(newQty); eqI.setNumberCarried(newQty); // Make sure the container accepts items // of this type and is not full if (eqTarget.canContain(this, eqI) == 1) { locName = eqTarget.getName(); addAll = true; mergeItem = true; } else { return null; } } // If locName is empty equip this item to its default location. // If there is more than one option return with an error. if (locName == null || locName.isEmpty()) { locName = getSingleLocation(eqI); if (locName.isEmpty()) { return null; } } // If it is to go into equipped, check for a specific slot it should be in. else if (locName.equalsIgnoreCase("Equipped")) { String singleLoc = getSingleLocation(eqI); if (!singleLoc.isEmpty()) { locName = singleLoc; } } // make sure we can add item to that slot in this EquipSet if (!canEquipItem(eSet, locName, eqI, eqTarget)) { return null; } if (eqI.isContainer()) { // don't merge containers mergeItem = false; } EquipSet existingSet = getEquipSetForItem(eSet, eqI); if (addAll && mergeItem && (existingSet != null)) { newQty = new Float(tempQty + getEquippedQty(eSet, eqI).floatValue()); existingSet.setQty(newQty); eqI.setQty(newQty); eqI.setNumberCarried(newQty); setDirty(true); if ((eqTarget != null) && eqTarget.isContainer()) { eqTarget.updateContainerContentsString(this); } return existingSet; } if ((eqTarget != null) && eqTarget.isContainer()) { eqTarget.insertChild(this, eqI); eqI.setParent(eqTarget); } // construct the new IdPath // new id is one larger than any // other id at this path level String id = getNewIdPath(eSet); // now create a new EquipSet to add // this Equipment item to EquipSet newSet = new EquipSet(id, locName, eqI.getName(), eqI); // set the Quantity of equipment eqI.setQty(newQty); newSet.setQty(newQty); addEquipSet(newSet); setDirty(true); return newSet; } /** * Move the equipset to a new unique path under its existing parent. * @param es The equipment set item to be moved. */ public void moveEquipSetToNewPath(EquipSet es) { String parentPath = es.getParentIdPath(); EquipSet parent = getEquipSetByIdPath(parentPath); String newPath = getNewIdPath(parent); es.setIdPath(newPath); } /** * Gets a 'safe' String representation * * @param key * @return a 'safe' String */ public String getSafeStringFor(PCStringKey key) { String s = factFacet.get(id, key); if (s == null) { s = Constants.EMPTY_STRING; } return s; } /** * Sets if ADD: level abilities should be processed when incrementing a * level. * * <p> * <b>Note</b>: This is kind of a hack used by the Kit code to allow a kit * to specify what the level abilities are. * * @param yesNo * Yes if level increases should process ADD: level abilities. */ public void setDoLevelAbilities(boolean yesNo) { processLevelAbilities = yesNo; } /** * Returns if level increases will process ADD: level abilities. * * @return <tt>true</tt> if ADD: level abilities will be processed. */ public boolean doLevelAbilities() { return processLevelAbilities; } /* * For debugging purposes Dumps contents of spell books to System.err * * static public void dumpSpells(final PlayerCharacter pc) { final List * bookList = pc.getSpellBooks(); for(int bookIdx = 0; bookIdx < * bookList.size(); ++bookIdx) { final String bookName = (String) * pc.getSpellBooks().get(bookIdx); * * System.err.println("=========="); System.err.println("Book:" + bookName); * final List casterList = pc.getSpellClassList(); for(int casterIdx = 0; * casterIdx < casterList.size(); ++casterIdx) { final PObject aCaster = * (PObject) casterList.get(casterIdx); final List spellList = * aCaster.getCharacterSpellList(); if (spellList == null) { continue; } * System.err.println("Class/Race:" + aCaster.getName()); * * for (Iterator i = spellList.iterator(); i.hasNext();) { final * CharacterSpell cs = (CharacterSpell) i.next(); * * for (Iterator csi = cs.getInfoListIterator(); csi.hasNext();) { final * SpellInfo sInfo = (SpellInfo) csi.next(); if * (bookName.equals(sInfo.getBook())) { * System.err.println(cs.getSpell().getOutputName() + sInfo.toString() + " * level:" + Integer.toString(sInfo.getActualLevel())); } } } } } } */ // -------------------------------------------------- // Feat/Ability stuff // -------------------------------------------------- public void adjustAbilities(final Category<Ability> aCategory, final BigDecimal arg) { if (arg.compareTo(BigDecimal.ZERO) == 0) { return; } if (theUserPoolBonuses == null) { theUserPoolBonuses = new HashMap<>(); } BigDecimal userMods = theUserPoolBonuses.get(aCategory); if (userMods != null) { userMods = userMods.add(arg); } else { userMods = arg; } theUserPoolBonuses.put(aCategory, userMods); setDirty(true); } public void setUserPoolBonus(final AbilityCategory aCategory, final BigDecimal anAmount) { if (theUserPoolBonuses == null) { theUserPoolBonuses = new HashMap<>(); } theUserPoolBonuses.put(aCategory, anAmount); } public double getUserPoolBonus(final AbilityCategory aCategory) { BigDecimal userBonus = null; if (theUserPoolBonuses != null) { userBonus = theUserPoolBonuses.get(aCategory); } if (userBonus == null) { return 0.0d; } return userBonus.doubleValue(); } public BigDecimal getTotalAbilityPool(final AbilityCategory aCategory) { Number basePool = getBasePool(aCategory); double bonus; if (AbilityCategory.FEAT.equals(aCategory)) { bonus = getBonusFeatPool(); } else { bonus = getTotalBonusTo("ABILITYPOOL", aCategory.getKeyName()); } // double bonus = getBonusValue("ABILITYPOOL", aCategory.getKeyName()); if (!aCategory.allowFractionalPool()) { bonus = Math.floor(bonus); } // User bonuses already handle the fractional pool flag. final double userBonus = getUserPoolBonus(aCategory); return BigDecimal.valueOf(basePool.floatValue() + bonus + userBonus); } private Number getBasePool(final AbilityCategory aCategory) { Number basePool = aCategory.getPoolFormula().resolve(this, getClass().toString()); if (!aCategory.allowFractionalPool()) { basePool = new Float(basePool.intValue()); } return basePool; } /** * Get the remaining Feat Points (or Skill Points if the GameMode uses a Point Pool). * * @return Number of remaining Feat Points */ public double getRemainingFeatPoolPoints() { if (Globals.getGameModeHasPointPool()) { return getSkillPoints(); } return getRemainingFeatPoints(true); } public BigDecimal getAvailableAbilityPool(final AbilityCategory aCategory) { return getTotalAbilityPool(aCategory).subtract(getAbilityPoolSpent(aCategory)); } /** * Get the number of remaining feat points. * * @param bIncludeBonus - Flag whether to include any bonus feat points * @return number of remaining feat points */ public double getRemainingFeatPoints(final boolean bIncludeBonus) { if (bIncludeBonus) { return getAvailableAbilityPool(AbilityCategory.FEAT).doubleValue(); } return getUserPoolBonus(AbilityCategory.FEAT); } private BigDecimal getAbilityPoolSpent(final AbilityCategory aCategory) { double spent = 0.0d; Collection<CNAbility> abilities = getPoolAbilities(aCategory, Nature.NORMAL); if (abilities != null) { for (final CNAbility cna : abilities) { Ability ability = cna.getAbility(); final int subfeatCount = getSelectCorrectedAssociationCount(cna); double cost = ability.getSafe(ObjectKey.SELECTION_COST).doubleValue(); if (ChooseActivation.hasNewChooseToken(ability)) { spent += Math.ceil(subfeatCount * cost); } else { int select = ability.getSafe(FormulaKey.SELECT).resolve(this, "").intValue(); double relativeCost = cost / select; if (aCategory.allowFractionalPool()) { spent += relativeCost; } else { spent += (int) Math.ceil(relativeCost); } } } } if (!aCategory.allowFractionalPool()) { return BigDecimal.valueOf((int) Math.ceil(spent)); } return BigDecimal.valueOf(spent); } public Ability getAbilityKeyed(AbilityCategory aCategory, String aKey) { for (Ability ability : getAbilityList(aCategory, Nature.NORMAL)) { if (ability.getKeyName().equals(aKey)) { return ability; } } for (Ability ability : getAbilityList(aCategory, Nature.VIRTUAL)) { if (ability.getKeyName().equals(aKey)) { return ability; } } for (Ability ability : getAbilityList(aCategory, Nature.AUTOMATIC)) { if (ability.getKeyName().equals(aKey)) { return ability; } } return null; } public boolean hasAbilityKeyed(final Category<Ability> cat, final String aKey) { return grantedAbilityFacet.hasAbilityKeyed(id, cat, aKey); } /** * Retrieve a list of all abilities held by the character in the specified * category. <br> * NB: Abilities are only returned in the category they are taken * in, so if parent category is supplied only those taken directly in the * parent category will be returned. e.g. If asking for feats, Power Attack * taken as a fighter feat will not be returned. You would need to query * fighter feats to get that. <br> * NB: Duplicate abilities will not be returned by this method. The order * of priority is normal, virtual then automatic. * * @param aCategory The ability category to be queried. * @return The list of abilities of the category regardless of nature. */ public List<Ability> getAggregateAbilityListNoDuplicates(final AbilityCategory aCategory) { List<Ability> aggregate = new ArrayList<>(); final Map<String, Ability> aHashMap = new HashMap<>(); for (Ability aFeat : getAbilityList(aCategory, Nature.NORMAL)) { if (aFeat != null) { aHashMap.put(aFeat.getKeyName(), aFeat); } } addUniqueAbilitiesToMap(aHashMap, getAbilityList(aCategory, Nature.VIRTUAL)); addUniqueAbilitiesToMap(aHashMap, getAbilityList(aCategory, Nature.AUTOMATIC)); aggregate.addAll(aHashMap.values()); return aggregate; } /** * @param aHashMap * @param abilityList TODO */ private static void addUniqueAbilitiesToMap(final Map<String, Ability> aHashMap, Collection<Ability> abilityList) { for (Ability vFeat : abilityList) { if (!aHashMap.containsKey(vFeat.getKeyName())) { aHashMap.put(vFeat.getKeyName(), vFeat); } } } private void processAbilityListsOnAdd(CDOMObject cdo, CDOMReference<? extends CDOMList<?>> ref) { for (CDOMList<?> list : ref.getContainedObjects()) { if (list instanceof AbilityList) { CDOMReference r = ref; processAbilityList(cdo, r); break; // Only do once } } } private void processAbilityList(CDOMObject cdo, CDOMReference<AbilityList> ref) { Collection<CDOMReference<Ability>> mods = cdo.getListMods(ref); for (CDOMReference<Ability> objref : mods) { Collection<Ability> objs = objref.getContainedObjects(); Collection<AssociatedPrereqObject> assoc = cdo.getListAssociations(ref, objref); for (Ability ab : objs) { if (ab == null) { Logging.log(Logging.LST_ERROR, "Missing object referenced in the ability list for '" + cdo + "' list is " + ref + ". Source " + cdo.getSourceURI()); continue; } for (AssociatedPrereqObject apo : assoc) { Nature nature = apo.getAssociation(AssociationKey.NATURE); CDOMSingleRef<AbilityCategory> acRef = apo.getAssociation(AssociationKey.CATEGORY); AbilityCategory cat = acRef.get(); if (ab.getSafe(ObjectKey.MULTIPLE_ALLOWED)) { List<String> choices = apo.getAssociation(AssociationKey.ASSOC_CHOICES); if (choices == null) { //CHOOSE:NOCHOICE can be unconditionally applied (must be STACK:YES) CNAbilitySelection cas = new CNAbilitySelection(CNAbilityFactory.getCNAbility(cat, nature, ab), ""); cas.addAllPrerequisites(apo.getPrerequisiteList()); applyAbility(cas, cdo); } else { for (final String choice : choices) { CNAbilitySelection cas = new CNAbilitySelection(CNAbilityFactory.getCNAbility(cat, nature, ab), choice); cas.addAllPrerequisites(apo.getPrerequisiteList()); applyAbility(cas, cdo); } } } else { CNAbilitySelection cas = new CNAbilitySelection(CNAbilityFactory.getCNAbility(cat, nature, ab)); cas.addAllPrerequisites(apo.getPrerequisiteList()); applyAbility(cas, cdo); } } } } cabFacet.update(id); } //WARNING: This is public only for testing, do NOT use without understanding what you are shortcutting!! public void applyAbility(CNAbilitySelection cas, Object source) { if (cas.hasPrerequisites()) { conditionalFacet.add(id, cas, source); } else { directAbilityFacet.add(id, cas, source); } } private void addTemplatesIfMissing(Collection<PCTemplate> templateList) { // if (!isImporting()) // { for (PCTemplate pct : templateList) { addTemplate(pct); } // } } public boolean hasSpellInSpellbook(Spell spell, String spellbookname) { for (CDOMObject po : getCDOMObjectList()) { List<CharacterSpell> csl = getCharacterSpells(po, spell, spellbookname, -1); if (csl != null && !csl.isEmpty()) { return true; } } return false; } public Collection<PCTemplate> getTemplatesAdded(CDOMObject po) { return addedTemplateFacet.getFromSource(id, po); } public void setTemplatesAdded(CDOMObject po, PCTemplate pct) { addedTemplateFacet.add(id, pct, po); } public boolean isClassSkill(Skill sk) { for (PCClass cl : getClassSet()) { if (isClassSkill(cl, sk)) { return true; } } return false; } private boolean isCrossClassSkill(Skill sk, PCClass pcc) { PCClass cl = getClassKeyed(pcc.getKeyName()); if (cl == null) { return false; } return skillCostFacet.isCrossClassSkill(id, pcc, sk); } private boolean isCrossClassSkill(Skill sk) { for (PCClass cl : getClassSet()) { if (isCrossClassSkill(sk, cl)) { return true; } } return false; } public SkillCost getSkillCostForClass(Skill sk, PCClass cl) { /* * Unfortunately class can be null if skill awarded by a master */ if (cl == null) { return sk.getSafe(ObjectKey.EXCLUSIVE) ? SkillCost.EXCLUSIVE : SkillCost.CROSS_CLASS; } cl = getClassKeyed(cl.getKeyName()); if (cl == null) { return sk.getSafe(ObjectKey.EXCLUSIVE) ? SkillCost.EXCLUSIVE : SkillCost.CROSS_CLASS; } return skillCostFacet.skillCostForPCClass(id, sk, cl); } public int getSelectCorrectedAssociationCount(ChooseDriver obj) { return getDetailedAssociationCount(obj) / obj.getSelectFormula().resolve(this, "").intValue(); } public List<String> getAssociationList(ChooseDriver obj) { ChooseInformation<?> info = obj.getChooseInfo(); if (info == null) { return Collections.emptyList(); } return getExpandedAssociations(obj, info); } /** * Return a list of the choice assications in an export compatible string * format. Note that this is not sufficient for the choice to be * reconstructed, so this format should never be saved, only output. * * @param obj The choice to be output. * @return The list of choices. */ public List<String> getAssociationExportList(ChooseDriver obj) { ChooseInformation<?> info = obj.getChooseInfo(); if (info == null) { return Collections.emptyList(); } return getExportAssociations(obj, info); } public boolean hasAssociations(ChooseDriver obj) { ChooseInformation<?> info = obj.getChooseInfo(); if (info == null) { return false; } List<?> selections = info.getChoiceActor().getCurrentlySelected(obj, this); return (selections != null) && !selections.isEmpty(); } public int getDetailedAssociationCount(ChooseDriver obj) { ChooseInformation<?> info = obj.getChooseInfo(); if (info == null) { return 0; } List<?> selections = info.getChoiceActor().getCurrentlySelected(obj, this); if ((selections == null) || selections.isEmpty()) { return 0; } return selections.size(); } private <T> List<String> getExpandedAssociations(ChooseDriver obj, ChooseInformation<T> info) { List<? extends T> selections = info.getChoiceActor().getCurrentlySelected(obj, this); if ((selections == null) || selections.isEmpty()) { return Collections.emptyList(); } List<String> ret = new ArrayList<>(selections.size()); for (T sel : selections) { ret.add(info.encodeChoice(sel)); } return ret; } private <T> List<String> getExportAssociations(ChooseDriver obj, ChooseInformation<T> info) { List<? extends T> selections = info.getChoiceActor().getCurrentlySelected(obj, this); if ((selections == null) || selections.isEmpty()) { return Collections.emptyList(); } List<String> ret = new ArrayList<>(selections.size()); for (T sel : selections) { ret.add(String.valueOf(sel)); } return ret; } public <T> void addAssoc(Object obj, AssociationListKey<T> ak, T o) { assocSupt.addAssoc(obj, ak, o); } public int getAssocCount(Object obj, AssociationListKey<?> ak) { return assocSupt.getAssocCount(obj, ak); } public <T> List<T> getAssocList(Object obj, AssociationListKey<T> ak) { return assocSupt.getAssocList(obj, ak); } public <T> List<T> removeAllAssocs(Object obj, AssociationListKey<T> ak) { return assocSupt.removeAllAssocs(obj, ak); } public <T> void removeAssoc(Object obj, AssociationListKey<T> ak, T o) { assocSupt.removeAssoc(obj, ak, o); } public <T> T getAssoc(Object obj, AssociationKey<T> ak) { return assocSupt.getAssoc(obj, ak); } public boolean hasAssocs(Object obj, AssociationKey<?> ak) { return assocSupt.hasAssocs(obj, ak); } public <T> void removeAssoc(Object obj, AssociationKey<T> ak) { assocSupt.removeAssoc(obj, ak); } public <T> void setAssoc(Object obj, AssociationKey<T> ak, T o) { assocSupt.setAssoc(obj, ak, o); } public boolean hasNonStatStat(PCStat stat) { // Check for a non stat, but only if it hasn't been reset to a stat if (!nonStatToStatFacet.contains(id, stat)) { if (nonStatStatFacet.contains(id, stat)) { return true; } } return false; } public boolean hasUnlockedStat(PCStat stat) { return unlockedStatFacet.contains(id, stat); } public Number getLockedStat(PCStat stat) { return statLockFacet.getLockedStat(id, stat); } public String getDescription(PObject pobj) { return getDescription(Collections.singletonList(pobj)); } public String getDescription(List<? extends Object> objList) { if (objList.isEmpty()) { return Constants.EMPTY_STRING; } PObject cdo; Object b = objList.get(0); if (b instanceof PObject) { cdo = (PObject) b; } else if (b instanceof CNAbility) { cdo = ((CNAbility) b).getAbility(); } else { Logging .errorPrint("Unable to resolve Description with object of type: " + b.getClass().getName()); return Constants.EMPTY_STRING; } List<Description> theDescriptions = cdo.getListFor(cdo.getDescriptionKey()); if (theDescriptions == null) { return Constants.EMPTY_STRING; } final StringBuilder sb = new StringBuilder(250); boolean needSpace = false; for (final Description desc : theDescriptions) { final String str = desc.getDescription(this, objList); if (!str.isEmpty()) { if (needSpace) { sb.append(' '); } sb.append(str); needSpace = true; } } return sb.toString(); } /** * This method gets the information about the levels at which classes and * domains may cast the spell. * * @param sp The spell to get the info for. * * @return Map containing the class levels and domains that may cast the * spell */ public HashMapToList<CDOMList<Spell>, Integer> getSpellLevelInfo(Spell sp) { HashMapToList<CDOMList<Spell>, Integer> hml = cache.get(MapKey.SPELL_PC_INFO, sp); if (hml == null) { hml = availSpellFacet.getSpellLevelInfo(id, sp); cache.addToMapFor(MapKey.SPELL_PC_INFO, sp, hml); } HashMapToList<CDOMList<Spell>, Integer> newhml = new HashMapToList<>(); newhml.addAllLists(hml); return newhml; } /** * Retrieve the character's existing version of this spell, if any. * @param po The source of the spell list for this spell (normally a PCClass) * @param spell The spell to be retrieved * @param owner The source of the spell (either the PCClass or the Domian) * @return The character's existing instance of the spell, or null if none. */ public CharacterSpell getCharacterSpellForSpell(PObject po, Spell spell, PObject owner) { List<CharacterSpell> cspells = new ArrayList<>(getCharacterSpells(po)); // Add in the spells granted by objects addBonusKnownSpellsToList(po, cspells); for (CharacterSpell cs : cspells) { Spell sp = cs.getSpell(); if (spell.equals(sp) && (cs.getOwner().equals(owner))) { return cs; } } return null; } /** * Get a list of CharacterSpells from the character spell list. * @param spellSource * @param aSpell * @param book * @param level * @return list of CharacterSpells from the character spell list */ public final List<CharacterSpell> getCharacterSpells(CDOMObject spellSource, final Spell aSpell, final String book, final int level) { List<CharacterSpell> csList = new ArrayList<>(getCharacterSpells(spellSource)); // Add in the spells granted by objects addBonusKnownSpellsToList(spellSource, csList); final List<CharacterSpell> aList = new ArrayList<>(); if (csList.isEmpty()) { return aList; } for (CharacterSpell cs : csList) { if ((aSpell == null) || cs.getSpell().equals(aSpell)) { final SpellInfo si = cs.getSpellInfoFor(book, level, null); if (si != null) { aList.add(cs); } } } return aList; } /** * Returns DC for a spell and SpellInfo. * @param sp the spell * @param cs TODO * @param si the spell info * @return DC for a spell and SpellInfo */ public int getDC(final Spell sp, CharacterSpell cs, final SpellInfo si) { CDOMObject ow = null; int spellLevel = 0; int metaDC = 0; spellLevel = si.getActualLevel(); ow = cs.getOwner(); String fixedDC = si.getFixedDC(); // TODO Temp fix for 1223858, better fix would be to move fixedDC to // spellInfo /* * TODO Need to evaluate how duplicative this logic is and what is * really necessary */ if (fixedDC != null && "INNATE".equalsIgnoreCase(si.getBook())) { return getVariableValue(fixedDC, "").intValue(); } // Check for a non class based fixed DC if (fixedDC != null && ow != null && !(ow instanceof PCClass)) { return getVariableValue(fixedDC, "").intValue(); } if (si.getFeatList() != null) { for (Ability metaFeat : si.getFeatList()) { spellLevel -= metaFeat.getSafe(IntegerKey.ADD_SPELL_LEVEL); metaDC = (int) (metaDC + BonusCalc.charBonusTo(metaFeat, "DC", "FEATBONUS", this)); } } return getDC(sp, null, spellLevel, metaDC, ow); } public int getDC(final Spell sp, PCClass aClass, int spellLevel, int metaDC, CDOMObject ow) { String bonDomain = ""; if (ow instanceof Domain) { bonDomain = "DOMAIN." + ow.getKeyName(); ClassSource source = getDomainSource((Domain) ow); if (source != null) { aClass = getClassKeyed(source.getPcclass().getKeyName()); } } boolean useStatFromSpell = false; String bonClass = ""; String spellType = ""; String classKey = ""; if ((aClass != null) || (ow instanceof PCClass)) { if ((aClass == null) || (ow instanceof PCClass)) { aClass = (PCClass) ow; } bonClass = "CLASS." + aClass.getKeyName(); classKey = "CLASS:" + aClass.getKeyName(); spellType = aClass.getSpellType(); useStatFromSpell = aClass.getSafe(ObjectKey.USE_SPELL_SPELL_STAT); } if (!(ow instanceof PCClass) && !(ow instanceof Domain)) { // get BASESPELLSTAT from spell itself useStatFromSpell = true; } // set the spell Level used in aPC.getVariableValue() // Explicitly should *not* set the dirty flag to true. spellLevelTemp = spellLevel; // must be done after spellLevel is set above int dc = getVariableValue(SettingsHandler.getGame().getSpellBaseDC(), classKey).intValue() + metaDC; dc += (int) getTotalBonusTo("DC", "ALLSPELLS"); if (useStatFromSpell) { // get the BASESPELLSTAT from the spell itself CDOMSingleRef<PCStat> stat = sp.get(ObjectKey.SPELL_STAT); if (stat != null) { dc += this.getStatModFor(stat.get()); } } if (!sp.getKeyName().isEmpty()) { dc += (int) getTotalBonusTo("DC", "SPELL." + sp.getKeyName()); } // DOMAIN.name if (!bonDomain.isEmpty()) { dc += (int) getTotalBonusTo("DC", bonDomain); } // CLASS.name if (!bonClass.isEmpty()) { dc += (int) getTotalBonusTo("DC", bonClass); } dc += (int) getTotalBonusTo("DC", "TYPE." + spellType); if (spellType.equals("ALL")) { for (Type aType : sp.getTrueTypeList(false)) { dc += (int) getTotalBonusTo("DC", "TYPE." + aType); } } for (SpellSchool aType : sp.getSafeListFor(ListKey.SPELL_SCHOOL)) { dc += (int) getTotalBonusTo("DC", "SCHOOL." + aType.toString()); } for (String aType : sp.getSafeListFor(ListKey.SPELL_SUBSCHOOL)) { dc += (int) getTotalBonusTo("DC", "SUBSCHOOL." + aType); } for (String aType : sp.getSafeListFor(ListKey.SPELL_DESCRIPTOR)) { dc += (int) getTotalBonusTo("DC", "DESCRIPTOR." + aType); } // Explicitly should *not* set the dirty flag to true. spellLevelTemp = 0; return dc; } /** * Returns concentration bonus for a spell and SpellInfo. * @param sp the spell * @param cs TODO * @param si the spell info * @return concentration bonus for a spell and SpellInfo */ public int getConcentration(final Spell sp, CharacterSpell cs, final SpellInfo si) { CDOMObject ow = null; int spellLevel = 0; int metaConcentration = 0; spellLevel = si.getActualLevel(); ow = cs.getOwner(); if (si.getFeatList() != null) { for (Ability metaFeat : si.getFeatList()) { spellLevel -= metaFeat.getSafe(IntegerKey.ADD_SPELL_LEVEL); metaConcentration = (int) (metaConcentration + BonusCalc.charBonusTo(metaFeat, "CONCENTRATION", "FEATBONUS", this)); } } return getConcentration(sp, cs, null, spellLevel, metaConcentration, ow); } public int getConcentration(final Spell sp, final CharacterSpell aSpell, PCClass aClass, int spellLevel, int metaConcentration, CDOMObject ow) { String bonDomain = ""; if (ow instanceof Domain) { bonDomain = "DOMAIN." + ow.getKeyName(); ClassSource source = getDomainSource((Domain) ow); if (source != null) { aClass = getClassKeyed(source.getPcclass().getKeyName()); } } boolean useStatFromSpell = false; String bonClass = ""; String spellType = ""; String classKey = ""; if ((aClass != null) || (ow instanceof PCClass)) { if ((aClass == null) || (ow instanceof PCClass)) { aClass = (PCClass) ow; } bonClass = "CLASS." + aClass.getKeyName(); classKey = "CLASS:" + aClass.getKeyName(); spellType = aClass.getSpellType(); useStatFromSpell = aClass.getSafe(ObjectKey.USE_SPELL_SPELL_STAT); } if (!(ow instanceof PCClass) && !(ow instanceof Domain)) { // get BASESPELLSTAT from spell itself useStatFromSpell = true; } // set the spell Level used in aPC.getVariableValue() // Explicitly should *not* set the dirty flag to true. spellLevelTemp = spellLevel; // must be done after spellLevel is set above int concentration = getVariableValue(aSpell, SettingsHandler.getGame().getSpellBaseConcentration(), classKey).intValue() + metaConcentration; concentration += (int) getTotalBonusTo("CONCENTRATION", "ALLSPELLS"); if (useStatFromSpell) { // get the BASESPELLSTAT from the spell itself CDOMSingleRef<PCStat> stat = sp.get(ObjectKey.SPELL_STAT); if (stat != null) { concentration += this.getStatModFor(stat.get()); } } if (!sp.getKeyName().isEmpty()) { concentration += (int) getTotalBonusTo("CONCENTRATION", "SPELL." + sp.getKeyName()); } // DOMAIN.name if (!bonDomain.isEmpty()) { concentration += (int) getTotalBonusTo("CONCENTRATION", bonDomain); } // CLASS.name if (!bonClass.isEmpty()) { concentration += (int) getTotalBonusTo("CONCENTRATION", bonClass); } concentration += (int) getTotalBonusTo("CONCENTRATION", "TYPE." + spellType); if (spellType.equals("ALL")) { for (Type aType : sp.getTrueTypeList(false)) { concentration += (int) getTotalBonusTo("CONCENTRATION", "TYPE." + aType); } } for (SpellSchool aType : sp.getSafeListFor(ListKey.SPELL_SCHOOL)) { concentration += (int) getTotalBonusTo("CONCENTRATION", "SCHOOL." + aType.toString()); } for (String aType : sp.getSafeListFor(ListKey.SPELL_SUBSCHOOL)) { concentration += (int) getTotalBonusTo("CONCENTRATION", "SUBSCHOOL." + aType); } for (String aType : sp.getSafeListFor(ListKey.SPELL_DESCRIPTOR)) { concentration += (int) getTotalBonusTo("CONCENTRATION", "DESCRIPTOR." + aType); } // Explicitly should *not* set the dirty flag to true. spellLevelTemp = 0; return concentration; } public boolean hasSkill(Skill skill) { return skillFacet.contains(id, skill); } public boolean hasTemplate(PCTemplate template) { return templateFacet.contains(id, template); } public Collection<PCStat> getStatSet() { return statFacet.getSet(id); } public boolean hasDefaultDomainSource() { return defaultDomainSource != null; } public void setDefaultDomainSource(ClassSource cs) { defaultDomainSource = cs; } public boolean addDomain(Domain domain) { return addDomain(domain, defaultDomainSource); } public boolean addDomain(Domain domain, ClassSource source) { boolean added = domainInputFacet.add(id, domain, source); if (added) { setDirty(true); } return added; } public boolean hasDomain(Domain domain) { return domainFacet.contains(id, domain); } public void removeDomain(Domain domain) { domainInputFacet.remove(id, domain); setDirty(true); } public boolean hasDomains() { return !domainFacet.isEmpty(id); } public int getDomainCount() { return domainFacet.getCount(id); } public Set<Domain> getDomainSet() { return domainFacet.getSet(id); } public ClassSource getDomainSource(Domain d) { return domainFacet.getSource(id, d); } public Map<String, String> getBonusStrings(String bonusString, String substring) { return bonusManager.getBonuses(bonusString, substring); } public boolean isApplied(BonusObj bonus) { return appliedBonusFacet.contains(id, bonus); } public SpellSupportForPCClass getSpellSupport(PCClass cl) { SpellSupportForPCClass ss = spellSupportFacet.get(id, cl); if (ss == null) { ss = new SpellSupportForPCClass(cl); spellSupportFacet.set(id, cl, ss); } return ss; } public Map<BonusObj, BonusManager.TempBonusInfo> getTempBonusMap(String sourceStr, String targetStr) { return bonusManager.getTempBonusMap(sourceStr, targetStr); } public String getBonusContext(BonusObj bonus, boolean shortForm) { return bonusManager.getBonusContext(bonus, shortForm); } public List<BonusPair> getStringListFromBonus(BonusObj bonus) { return bonusManager.getStringListFromBonus(bonus); } public void setApplied(BonusObj bonusObj, boolean bool) { if (bool) { appliedBonusFacet.add(id, bonusObj); } else { appliedBonusFacet.remove(id, bonusObj); } } public void setSubstitutionLevel(PCClass pcc, PCClassLevel originalClassLevel) { try { PCClassLevel clvl = originalClassLevel.clone(); clvl.put(StringKey.QUALIFIED_KEY, pcc.getQualifiedKey()); classFacet.setClassLevel(id, pcc, clvl); } catch (CloneNotSupportedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public PCClassLevel getActiveClassLevel(PCClass pcc, int lvl) { return classFacet.getClassLevel(id, pcc, lvl); } public boolean hasLanguage(Language lang) { return languageFacet.contains(id, lang); } public boolean hasClass() { return !classFacet.isEmpty(id); } public void removeClass(PCClass pcc) { classFacet.removeClass(id, pcc); } public void addClass(PCClass pcc) { classFacet.addClass(id, pcc); } public final int getLevel(PCClass pcc) { return classFacet.getLevel(id, pcc); } /** * set the level to arg without impacting spells, hp, or anything else - use * this with great caution only. */ public final void setLevelWithoutConsequence(PCClass pcc, final int level) { classFacet.setLevel(id, pcc, level); cabFacet.update(id); } public boolean hasEquipment() { return !equipmentFacet.isEmpty(id); } private Set<Ability> getAbilityList(Category<Ability> cat, Nature nature) { Set<Ability> newSet = new HashSet<>(); Collection<CNAbility> cnas = grantedAbilityFacet.getPoolAbilities(id, cat, nature); for (CNAbility cna : cnas) { newSet.add(cna.getAbility()); } return newSet; } public boolean containsKit(Kit kit) { return kitFacet.contains(id, kit); } /* * Yes, this method really is what it says. The primary reason for this * being in PlayerCharacter is that I don't want to export id at this time * (it's private to avoid changing too much outside of PlayerCharacter at * this time). In the future, the Unit Tests should behave better - but I * think that generally goes along with Equipment * Location/Equipped/NumberEquipped/NumberCarried all being made consistent * (they are highly correlated, but no control is exerted over them by * Equipment to ensure appropriate states are maintained) */ public void doAfavorForAunitTestThatIgnoresEquippingRules() { equippedFacet.reset(id); } public void processAddition(CDOMObject cdo) { for (CDOMReference<PCTemplate> tr : cdo.getSafeListFor(ListKey.TEMPLATE)) { addTemplatesIfMissing(tr.getContainedObjects()); } for (CDOMReference ref : cdo.getModifiedLists()) { processAbilityListsOnAdd(cdo, ref); } } public void processRemoval(CDOMObject cdo) { conditionalFacet.removeAll(id, cdo); directAbilityFacet.removeAll(id, cdo); //setDirty(true); } public void addWeaponBonus(CDOMObject owner, WeaponProf choice) { wpBonusFacet.add(id, choice, owner); } public List<? extends WeaponProf> getBonusWeaponProfs(CDOMObject owner) { return wpBonusFacet.getSet(id, owner); } public void removeWeaponBonus(CDOMObject owner, WeaponProf choice) { wpBonusFacet.remove(id, choice, owner); } public void addFavoredClass(PCClass cls, Object source) { favClassFacet.add(id, cls, source); } public void removeFavoredClass(PCClass cls, Object source) { favClassFacet.remove(id, cls, source); } public PCClass getLegacyFavoredClass() { List<? extends PCClass> list = favClassFacet.getSet(id, this); if (list.isEmpty()) { return null; } return list.get(0); } public void addWeaponProf(Object owner, WeaponProf choice) { alWeaponProfFacet.add(id, choice, owner); } public void removeWeaponProf(Object owner, WeaponProf choice) { alWeaponProfFacet.remove(id, choice, owner); } /** * WARNING: Use this method SPARINGLY... and only for transition to the * facet model. It is NOT an excuse to throw around a PlayerCharacter object * when unnecessary * * @return The id of the character as used by the facets. */ public CharID getCharID() { return id; } private int getSpellBookCount() { return spellBookFacet.getCount(id); } public boolean hasSpellBook(String bookName) { return spellBookFacet.containsBookNamed(id, bookName); } private Load getLoadType() { return loadFacet.getLoadType(id); } public void addArmorProf(Object owner, ArmorProf ap) { armorProfListFacet.add(id, ap, owner); } public void removeArmorProf(Object owner, ArmorProf ap) { armorProfListFacet.remove(id, ap, owner); } public void addShieldProf(Object owner, ShieldProf sp) { shieldProfListFacet.add(id, sp, owner); } public void removeShieldProf(Object owner, ShieldProf sp) { shieldProfListFacet.remove(id, sp, owner); } public boolean hasFollowers() { return !followerFacet.isEmpty(id); } public void addAutoEquipment(Equipment e, Object obj) { autoListEquipmentFacet.add(id, e, obj); } public void removeAutoEquipment(Equipment e, Object obj) { autoListEquipmentFacet.remove(id, e, obj); } public void addMonCSkill(Skill skill, Object obj) { monCSkillFacet.add(id, skill, obj); } public void removeMonCSkill(Skill skill, Object obj) { monCSkillFacet.remove(id, skill, obj); } public Collection<? extends SpellProhibitor> getProhibitedSchools(PCClass source) { List<SpellProhibitor> list = new ArrayList<>(); list.addAll(prohibitedSchoolFacet.getSet(id, source)); list.addAll(spellProhibitorFacet.getSet(id, source)); return list; } public boolean containsProhibitedSchools(Object source) { return prohibitedSchoolFacet.containsFrom(id, source); } public void addProhibitedSchool(SpellProhibitor prohibSchool, Object source) { prohibitedSchoolFacet.add(id, prohibSchool, source); } public void removeProhibitedSchools(Object source) { prohibitedSchoolFacet.removeAll(id, source); } private boolean hasCharacterSpells(CDOMObject cdo) { return activeSpellsFacet.containsFrom(id, cdo); } public Collection<? extends CharacterSpell> getCharacterSpells(CDOMObject cdo) { return activeSpellsFacet.getSet(id, cdo); } public Collection<CharacterSpell> getCharacterSpells(PObject spellSource, int level) { List<CharacterSpell> csList = new ArrayList<>(getCharacterSpells(spellSource)); // Add in the spells granted by objects addBonusKnownSpellsToList(spellSource, csList); List<CharacterSpell> aList = new ArrayList<>(); for (CharacterSpell cs : csList) { if (cs.hasSpellInfoFor(level)) { aList.add(cs); } } return aList; } public Collection<CharacterSpell> getCharacterSpells(PObject spellSource, String bookName) { List<CharacterSpell> csList = new ArrayList<>(getCharacterSpells(spellSource)); // Add in the spells granted by objects addBonusKnownSpellsToList(spellSource, csList); List<CharacterSpell> aList = new ArrayList<>(); for (CharacterSpell cs : csList) { if (cs.hasSpellInfoFor(bookName)) { aList.add(cs); } } return aList; } public int getCharacterSpellCount(CDOMObject cdo) { return activeSpellsFacet.getCountFrom(id, cdo); } public void addCharacterSpell(CDOMObject cdo, CharacterSpell cs) { activeSpellsFacet.add(id, cs, cdo); } public void removeCharacterSpell(CDOMObject cdo, CharacterSpell cs) { activeSpellsFacet.remove(id, cs, cdo); } private boolean containsCharacterSpell(CDOMObject cdo, CharacterSpell cs) { return activeSpellsFacet.containsFrom(id, cs, cdo); } public void addBonus(BonusObj bonus, CDOMObject source) { addedBonusFacet.add(id, bonus, source); } public List<? extends BonusObj> getAddedBonusList(CDOMObject source) { return addedBonusFacet.getSet(id, source); } public void addSaveableBonus(BonusObj bonus, CDOMObject source) { saveableBonusFacet.add(id, bonus, source); } public List<? extends BonusObj> getSaveableBonusList(CDOMObject source) { return saveableBonusFacet.getSet(id, source); } public void removeSaveableBonus(BonusObj bonus, CDOMObject source) { saveableBonusFacet.remove(id, bonus, source); } public void addGlobalCost(SkillCost sc, Skill skill, Object obj) { globalAddedSkillCostFacet.add(id, sc, skill, obj); } public void removeGlobalCost(SkillCost sc, Skill skill, Object obj) { globalAddedSkillCostFacet.remove(id, sc, skill, obj); } public void addLocalCost(PCClass pcc, Skill skill, SkillCost sc, Object owner) { localAddedSkillCostFacet.add(id, pcc, sc, skill, owner); } public void removeLocalCost(PCClass pcc, Skill skill, SkillCost sc, Object owner) { localAddedSkillCostFacet.remove(id, pcc, sc, skill, owner); } public String getSubClassName(PCClass cl) { return subClassFacet.get(id, cl); } public void setSubClassName(PCClass cl, String key) { subClassFacet.set(id, cl, key); } public boolean hasTempApplied(CDOMObject mod) { return bonusManager.hasTempBonusesApplied(mod); } public Collection<BonusContainer> getBonusContainerList() { List<BonusContainer> list = new ArrayList<>(getCDOMObjectList()); list.add(ageSetFacet.get(id)); GameMode gm = SettingsHandler.getGame(); if (gm.isPurchaseStatMode()) { PointBuyMethod pbm = gm.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(PointBuyMethod.class, gm.getPurchaseModeMethodName()); list.add(pbm); } return list; } public SkillCost skillCostForPCClass(Skill sk, PCClass aClass) { //For safety PCClass cl = getClassKeyed(aClass.getKeyName()); return skillCostFacet.skillCostForPCClass(id, sk, cl == null ? aClass : cl); } public boolean isClassSkill(PCClass aClass, Skill sk) { //For safety PCClass cl = getClassKeyed(aClass.getKeyName()); return skillCostFacet.isClassSkill(id, cl == null ? aClass : cl, sk); } public boolean isQualified(CDOMObject po) { return po.qualifies(this, po); } public void reInheritClassLevels(PCClass pcc) { try { for (PCClassLevel pcl : pcc.getOriginalClassLevelCollection()) { classFacet.setClassLevel(id, pcc, pcl); } } catch (CloneNotSupportedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void checkSkillModChange() { List<PCClass> newClasses = getClassList(); Collection<PCLevelInfo> levelInfo = getLevelInfo(); int levelIndex = 1; for (PCLevelInfo lvlInfo : levelInfo) { Map<String, PCClass> classMap = new HashMap<>(); for (PCClass pcClass : newClasses) { classMap.put(pcClass.getKeyName(), pcClass); } final String classKeyName = lvlInfo.getClassKeyName(); PCClass currClass = classMap.get(classKeyName); if (currClass == null) { Logging.errorPrint("No PCClass found for '" + classKeyName + "' in character's class list: " + newClasses); return; } PCClassLevel classLevel = getActiveClassLevel(currClass, lvlInfo.getClassLevel()); checkSkillModChangeForLevel(currClass, lvlInfo, classLevel, levelIndex++); } } public void checkSkillModChangeForLevel(PCClass pcClass, PCLevelInfo pi, PCClassLevel classLevel, int characterLevel) { int newSkillPointsGained = pcClass.getSkillPointsForLevel(this, classLevel, characterLevel); if (pi.getClassKeyName().equals(pcClass.getKeyName())) { final int formerGained = pi.getSkillPointsGained(this); if (newSkillPointsGained != formerGained) { pi.setSkillPointsGained(this, newSkillPointsGained); newSkillPointsGained = pi.getSkillPointsGained(this); pi.setSkillPointsRemaining(pi.getSkillPointsRemaining() + newSkillPointsGained - formerGained); setSkillPool(pcClass, pcClass.getSkillPool(this) + newSkillPointsGained - formerGained); } } } /** * Add a chronicle entry. * @param chronicleEntry The entry to be added. */ public void addChronicleEntry(ChronicleEntry chronicleEntry) { chronicleEntryFacet.add(id, chronicleEntry); } /** * Remove a chronicle entry. * @param chronicleEntry The entry to be removed. */ public void removeChronicleEntry(ChronicleEntry chronicleEntry) { chronicleEntryFacet.remove(id, chronicleEntry); } public BioSet getBioSet() { return bioSetFacet.get(id); } public HitDie getLevelHitDie(PCClass pcClass, final int classLevel) { return hitPointFacet.getLevelHitDie(id, pcClass, classLevel); } /** * Rolls hp for the current level according to the rules set in options. * * @param pcClass TODO * @param aLevel * @param first */ /* * REFACTOR This really needs to be part of the PCClassLevel importing into * a PlayerCharacter? Some thought needs to be put into where this stuff is * stored - should PCLevelInfo be adapted to store all of the non-static * information about a PCClassLevel? */ public void rollHP(PCClass pcClass, int aLevel, boolean first) { hitPointFacet.rollHP(id, pcClass, aLevel, first); setDirty(true); } public void setHP(PCClassLevel pcl, Integer hp) { hitPointFacet.set(id, pcl, hp); } public Integer getHP(PCClassLevel pcl) { return hitPointFacet.get(id, pcl); } public void removeHP(PCClassLevel pcl) { hitPointFacet.remove(id, pcl); } public void addClassSpellList(CDOMListObject<Spell> list, PCClass pcClass) { spellListFacet.add(id, list, pcClass); } public List<? extends CDOMList<Spell>> getSpellLists(CDOMObject cdo) { return spellListFacet.getSet(id, cdo); } public boolean hasSpellList(CDOMObject cdo, CDOMList<Spell> list) { return spellListFacet.containsFrom(id, list, cdo); } public void setSpellLists(PCClass pcClass) { classSpellListFacet.process(id, pcClass); } public void addDefaultSpellList(PCClass pcc) { classSpellListFacet.addDefaultSpellList(id, pcc); } double getSizeBonusTo(SizeAdjustment sizeAdjustment, final String bonusType, final List<String> typeList, double defaultValue) { for (String type : typeList) { /* * TODO: The standard for these bonuses should probably be TYPE=, but * the bonus objects only correctly match TYPE. The bonus objects * probably need to be reevaluated to standardize this usage */ final double a = BonusCalc.charBonusTo(sizeAdjustment, bonusType, "TYPE." + type, this); if (!CoreUtility.doublesEqual(a, 0.0)) { defaultValue = a; break; } } return defaultValue; } /** * Adds to the provided list any spells that have been granted to the character's class by abilities * through the use of SPELLKNOWN:CLASS tags. * * @param aClass The character class owning the spell list. * @param cSpells The list of spells to be updated. */ public void addBonusKnownSpellsToList(CDOMObject aClass, List<CharacterSpell> cSpells) { if (!(aClass instanceof PCClass)) { return; } ClassSpellList classSpellList = aClass.get(ObjectKey.CLASS_SPELLLIST); for (Integer spellLevel : knownSpellFacet.getScopes2(id, classSpellList)) { for (Spell spell : knownSpellFacet.getSet(id, classSpellList, spellLevel)) { CharacterSpell acs = null; Collection<? extends CharacterSpell> characterSpells = getCharacterSpells(grantedSpellCache); for (CharacterSpell cs : characterSpells) { Spell sp = cs.getSpell(); if (spell.equals(sp) && (cs.getOwner().equals(aClass))) { acs = cs; break; } } if (acs == null) { acs = new CharacterSpell(aClass, spell); acs.addInfo(spellLevel, 1, Globals.getDefaultSpellBook()); addCharacterSpell(grantedSpellCache, acs); } if (!cSpells.contains(acs)) { cSpells.add(acs); } } } } public boolean hasBonusWeaponProfs(CDOMObject owner) { return wpBonusFacet.containsFrom(id, owner); } public void addUserSpecialAbility(SpecialAbility sa, CDOMObject source) { userSpecialAbilityFacet.add(id, sa, source); } public CharacterDisplay getDisplay() { return display; } public List<WeaponProf> getWeaponProfsInTarget(CDOMGroupRef<WeaponProf> master) { return changeProfFacet.getWeaponProfsInTarget(id, master); } public void setSubstitutionClassName(PCClassLevel lvl, String subClassKey) { substitutionClassFacet.set(id, lvl, subClassKey); } public void removeSubstitutionClassName(PCClassLevel lvl) { substitutionClassFacet.remove(id, lvl); } public void setStat(PCStat stat, int value) { statValueFacet.set(id, stat, value); } public int getStat(PCStat stat) { return statValueFacet.get(id, stat).intValue(); } public int recalcSkillPointMod(PCClass pcClass, final int characterLevel) { // int spMod = getSkillPoints(); int lockedMonsterSkillPoints; int spMod = pcClass.getSafe(FormulaKey.START_SKILL_POINTS).resolve(this, pcClass.getQualifiedKey()).intValue(); spMod += (int) getTotalBonusTo("SKILLPOINTS", "NUMBER"); if (pcClass.isMonster()) { lockedMonsterSkillPoints = (int) getTotalBonusTo("MONSKILLPTS", "LOCKNUMBER"); if (lockedMonsterSkillPoints > 0) { spMod = lockedMonsterSkillPoints; } else if (characterLevel == 1) { int monSkillPts = (int) getTotalBonusTo("MONSKILLPTS", "NUMBER"); if (monSkillPts != 0) { spMod = monSkillPts; } } if (characterLevel != 1) { // If this level is one that is not entitled to skill points // based // on the monster's size, zero out the skills for this level final int nonSkillHD = (int) getTotalBonusTo("MONNONSKILLHD", "NUMBER"); if (characterLevel <= nonSkillHD) { spMod = 0; } } } spMod = updateBaseSkillMod(pcClass, spMod); if (characterLevel == 1) { if (!SettingsHandler.getGame().isPurchaseStatMode()) { poolAmount = 0; } spMod *= getRace().getSafe(IntegerKey.INITIAL_SKILL_MULT); if (ageFacet.getAge(id) <= 0) { // Only generate a random age if the user hasn't set one! bioSetFacet.get(id).randomize("AGE", this); } } else { spMod *= Globals.getSkillMultiplierForLevel(characterLevel); } return spMod; } private int updateBaseSkillMod(PCClass pcClass, int spMod) { // skill min is 1, unless class gets 0 skillpoints per level (for second // apprentice class) final int skillMin = (spMod > 0) ? 1 : 0; if (pcClass.getSafe(ObjectKey.MOD_TO_SKILLS)) { spMod += (int) getStatBonusTo("MODSKILLPOINTS", "NUMBER"); if (spMod < 1) { spMod = 1; } } // Race modifiers apply after Intellegence. BUG 577462 Formula safe = getRace().getSafe(FormulaKey.SKILL_POINTS_PER_LEVEL); spMod += safe.resolve(this, "").intValue(); spMod = Math.max(skillMin, spMod); // Minimum 1, not sure if bonus // level can be < 1, better safe than sorry for (PCTemplate template : getTemplateSet()) { spMod += template.getSafe(IntegerKey.BONUS_CLASS_SKILL_POINTS); } return spMod; } public void removeDomainSpellCount(PCClass pcc) { domainSpellCountFacet.remove(id, pcc); } public Integer getDomainSpellCount(PCClass pcc) { return domainSpellCountFacet.get(id, pcc); } public void setDomainSpellCount(PCClass pcc, int i) { domainSpellCountFacet.set(id, pcc, i); } public Integer getSkillPool(PCClass pcc) { return skillPoolFacet.get(id, pcc); } public void setSkillPool(PCClass pcc, int skillPool) { skillPoolFacet.set(id, pcc, skillPool); setDirty(true); } public void setSkillOrder(Skill skill, int outputindex) { skillOutputOrderFacet.set(id, skill, outputindex); } public Integer getSkillOrder(Skill skill) { return skillOutputOrderFacet.get(id, skill); } public int getBaseStatFor(PCStat stat) { return statCalcFacet.getBaseStatFor(id, stat); } public int getTotalStatFor(PCStat stat) { return statCalcFacet.getTotalStatFor(id, stat); } public int getStatModFor(PCStat stat) { return statCalcFacet.getStatModFor(id, stat); } public int getModForNumber(int aNum, PCStat stat) { return statCalcFacet.getModFornumber(id, aNum, stat); } public void removeNote(NoteItem note) { noteItemFacet.remove(id, note); setDirty(true); } public void removeSkillRankValue(Skill sk, PCClass cl) { //Hedge bets on the class PCClass localClass = (cl == null) ? null : getClassKeyed(cl.getKeyName()); removeSkillRankForLocalClass(sk, localClass); } public void removeSkillRankForLocalClass(Skill sk, PCClass localClass) { skillRankFacet.remove(id, sk, localClass); } public void setSkillRankValue(Skill sk, PCClass pcc, double value) { //hedge bets on the class PCClass localClass = (pcc == null) ? null : getClassKeyed(pcc.getKeyName()); skillRankFacet.set(id, sk, localClass, value); } /** * Retrieve the classes that have ranks in this skill. NB: For granted ranks * this may include null. * @param sk The skill to be checked. * @return The collection of classes with ranks - may include null as a PCClass. */ public Collection<PCClass> getSkillRankClasses(Skill sk) { return skillRankFacet.getClasses(id, sk); } /** * returns ranks taken specifically in skill * * @return ranks taken in skill */ public Float getRank(Skill sk) { return skillRankFacet.getRank(id, sk); } /** * @return the allowDebt */ public boolean isAllowDebt() { Boolean ad = allowDebtFacet.get(id); return (ad == null) ? SettingsHandler.getGearTab_AllowDebt() : ad; } /** * @return the ignoreCost */ public boolean isIgnoreCost() { Boolean ic = ignoreCostFacet.get(id); return (ic == null) ? SettingsHandler.getGearTab_IgnoreCost() : ic; } public Double getSkillRankForClass(Skill sk, PCClass pcc) { //Yes, the check for "local" class is required (try down-ranking a skill) PCClass localClass = (pcc == null) ? null : getClassKeyed(pcc.getKeyName()); return getSkillRankForLocalClass(sk, localClass); } public Double getSkillRankForLocalClass(Skill sk, PCClass localClass) { return skillRankFacet.get(id, sk, localClass); } public int getKnownSpellCountForLevel(CDOMList<Spell> list, int level) { return knownSpellFacet.getSize(id, list, level); } public Collection<Spell> getSpellsIn(CDOMList<Spell> list, final int level) { return availSpellFacet.getSet(id, list, level); } public List<Spell> getAllSpellsInLists(List<? extends CDOMList<Spell>> spellLists) { List<Spell> spellList = new ArrayList<>(); for (CDOMList<Spell> list : availSpellFacet.getScopes1(id)) { if (spellLists.contains(list)) { for (int lvl : availSpellFacet.getScopes2(id, list)) { for (Spell spell : availSpellFacet.getSet(id, list, lvl)) { spellList.add(spell); } } } } return spellList; } public void calculateKnownSpellsForClassLevel(PCClass pcc) { if (!pcc.containsListFor(ListKey.KNOWN_SPELLS) || importing || !autoKnownSpells) { return; } // If this class has at least one entry in the "Known spells" tag // And we are set up to automatically assign known spells... List<? extends CDOMList<Spell>> spellLists = getSpellLists(pcc); SpellSupportForPCClass spellSupport = getSpellSupport(pcc); // Recalculate the number of spells per day of each level // that this chracter can cast in this class. spellSupport.calcCastPerDayMapForLevel(this); // Get the maximum spell level that this character can cast. final int maxCastableLevel = spellSupport.getMaxCastLevel(); for (CDOMList<Spell> list : spellLists) { for (int spellLevel : availSpellFacet.getScopes2(id, list)) { if (spellLevel <= maxCastableLevel) { for (Spell spell : availSpellFacet.getSet(id, list, spellLevel)) { if (spellSupport.isAutoKnownSpell(spell, spellLevel, true, this)) { CharacterSpell cs = getCharacterSpellForSpell(pcc, spell, pcc); if (cs == null) { // Create a new character spell for this level. cs = new CharacterSpell(pcc, spell); cs.addInfo(spellLevel, 1, Globals.getDefaultSpellBook()); addCharacterSpell(pcc, cs); } else { if (cs.getSpellInfoFor( Globals.getDefaultSpellBook(), spellLevel) == null) { cs.addInfo(spellLevel, 1, Globals.getDefaultSpellBook()); } else { // already know this one } } } } } } } for (Domain d : getDomainSet()) { if (pcc.getKeyName().equals( getDomainSource(d).getPcclass().getKeyName())) { DomainApplication.addSpellsToClassForLevels(this, d, pcc, 0, maxCastableLevel); } } } public void removeKnownSpellsForClassLevel(PCClass pcc) { if (!pcc.containsListFor(ListKey.KNOWN_SPELLS) || importing || !autoKnownSpells) { return; } if (!hasCharacterSpells(pcc)) { return; } SpellSupportForPCClass spellSupport = getSpellSupport(pcc); List<? extends CDOMList<Spell>> lists = getSpellLists(pcc); List<CharacterSpell> spellsToBeRemoved = new ArrayList<>(); for (final CharacterSpell charSpell : getCharacterSpells(pcc)) { final Spell aSpell = charSpell.getSpell(); // Check that the character can still cast spells of this level. final Integer[] spellLevels = SpellLevel.levelForKey(aSpell, lists, this); for (final Integer spellLevel : spellLevels) { if (spellLevel == -1) { continue; } final boolean isKnownAtThisLevel = spellSupport.isAutoKnownSpell(aSpell, spellLevel, true, this); if (!isKnownAtThisLevel) { spellsToBeRemoved.add(charSpell); } } } for (CharacterSpell characterSpell : spellsToBeRemoved) { removeCharacterSpell(pcc, characterSpell); } } public void addTemplateFeat(CDOMObject template, CNAbilitySelection as) { templateFeatFacet.add(id, as, template); } public List<? extends CNAbilitySelection> getTemplateFeatList(CDOMObject template) { return templateFeatFacet.getSet(id, template); } public Collection<CNAbility> getCNAbilities() { Set<CNAbility> set = new HashSet<>(grantedAbilityFacet.getCNAbilities(id)); return set; } public Collection<CNAbility> getCNAbilities(Category<Ability> cat, Nature n) { if (!cat.getParentCategory().equals(cat)) { throw new IllegalArgumentException( "Category for getCNAbilities must be parent category"); } Set<CNAbility> set = new HashSet<>(grantedAbilityFacet.getCNAbilities(id, cat, n)); return set; } public List<?> getDetailedAssociations(ChooseDriver cd) { ChooseInformation<?> chooseInfo = cd.getChooseInfo(); return chooseInfo.getChoiceActor().getCurrentlySelected(cd, this); } public List<CNAbility> getMatchingCNAbilities(Ability ability) { List<CNAbility> list = new ArrayList<>(grantedAbilityFacet.getCNAbilities(id, ability)); return list; } public List<CNAbility> getCNAbilities(Category<Ability> cat) { if (!cat.getParentCategory().equals(cat)) { throw new IllegalArgumentException( "Category for getCNAbilities must be parent category, was: " + cat); } List<CNAbility> list = new ArrayList<>(grantedAbilityFacet.getCNAbilities(id, cat)); return list; } public List<CNAbility> getPoolAbilities(Category<Ability> cat) { List<CNAbility> list = new ArrayList<>(grantedAbilityFacet.getPoolAbilities(id, cat)); return list; } public Collection<CNAbility> getPoolAbilities(Category<Ability> cat, Nature n) { Set<CNAbility> set = new HashSet<>(grantedAbilityFacet.getPoolAbilities(id, cat, n)); return set; } public Collection<CNAbilitySelection> getSaveAbilities() { return svAbilityFacet.getSet(id); } public CNAbility getBonusLanguageAbility() { return bonusLanguageAbility; } public void setAllowInteraction(boolean b) { if (!b && !allowInteraction) { Logging .errorPrint("Internal Error: Re-entrant prohibition of interaction"); } allowInteraction = b; } public boolean isAllowInteraction() { return allowInteraction; } public void associateSelection(AbilitySelection as, CNAbilitySelection cnas) { astocnasFacet.set(id, as, cnas); } public CNAbilitySelection getAssociatedSelection(AbilitySelection as) { return astocnasFacet.get(id, as); } public void addSavedAbility(CNAbilitySelection cnas, Object owner, Object location) { svAbilityFacet.add(id, cnas); addAbility(cnas, owner, location); } public void addAbility(CNAbilitySelection cnas, @SuppressWarnings("UnusedParameters") final Object owner, Object location) { //TODO Need to handle owner if (cnas.hasPrerequisites()) { conditionalFacet.add(id, cnas, location); } else { directAbilityFacet.add(id, cnas, location); } if (!importing) { AbilityUtilities.finaliseAbility(this, cnas); } } public void removeAbility(CNAbilitySelection cnas, @SuppressWarnings("UnusedParameters") Object owner, Object location) { //TODO Need to handle owner if (cnas.hasPrerequisites()) { conditionalFacet.remove(id, cnas, location); } else { directAbilityFacet.remove(id, cnas, location); } CDOMObjectUtilities.removeAdds(cnas.getCNAbility().getAbility(), this); setDirty(true); calcActiveBonuses(); } public void removeSavedAbility(CNAbilitySelection cnas, Object owner, Object location) { svAbilityFacet.remove(id, cnas); removeAbility(cnas, owner, location); } public List<String> getConsolidatedAssociationList(CDOMObject cdo) { if (cdo instanceof Ability) { List<String> list = new ArrayList<>(); List<CNAbility> cnabilities = getMatchingCNAbilities((Ability) cdo); for (CNAbility cna : cnabilities) { list.addAll(getAssociationList(cna)); } return list; } else if (cdo instanceof ChooseDriver) { return getAssociationList((ChooseDriver) cdo); } else { //Can't really do a message here because this is heavily done by BonusManager // Logging // .errorPrint("Consolidated Association List requested for object of type " // + cdo.getClass() + " but it is not a ChooseDriver"); return Collections.emptyList(); } } public boolean hasAbilityInPool(AbilityCategory aCategory) { return grantedAbilityFacet.hasAbilityInPool(id, aCategory); } public <T> void addModifier(VarModifier<T> modifier, VarScoped vs, VarScoped source) { ScopeInstance inst = scopeFacet.get(id, source.getLocalScopeName(), source); solverManagerFacet.addModifier(id, modifier, vs, inst); } public Object getGlobal(String varName) { return resultFacet.getGlobalVariable(id, varName); } public Object getLocal(CDOMObject owner, String varName) { return resultFacet.getLocalVariable(id, owner, varName); } public String getControl(String string) { return controller.get(ObjectKey.getKeyFor(String.class, "*" + string)); } public boolean hasControl(String string) { return controller.get(ObjectKey.getKeyFor(String.class, "*" + string)) != null; } }