/* * Copyright (c) Thomas Parker, 2012. * * This program 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 program 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., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package pcgen.cdom.facet; import pcgen.base.util.RandomUtil; import pcgen.cdom.base.CDOMObject; import pcgen.cdom.base.Constants; import pcgen.cdom.content.HitDie; import pcgen.cdom.content.Processor; import pcgen.cdom.enumeration.CharID; import pcgen.cdom.enumeration.ObjectKey; import pcgen.cdom.facet.analysis.LevelFacet; import pcgen.cdom.facet.base.AbstractAssociationFacet; import pcgen.cdom.facet.event.DataFacetChangeEvent; import pcgen.cdom.facet.event.DataFacetChangeListener; import pcgen.cdom.facet.model.ClassFacet; import pcgen.cdom.facet.model.RaceFacet; import pcgen.cdom.facet.model.TemplateFacet; import pcgen.cdom.inst.PCClassLevel; import pcgen.core.PCClass; import pcgen.core.PCTemplate; import pcgen.core.PlayerCharacter; import pcgen.core.SettingsHandler; /** * HitPointFacet stores information about hit points for a Player Character. * Specifically this Facet stores the number of hit points granted to a Player * Character for each PCClassLevel possessed by the Player Character. * * @author Thomas Parker (thpr [at] yahoo.com) */ public class HitPointFacet extends AbstractAssociationFacet<CharID, PCClassLevel, Integer> implements DataFacetChangeListener<CharID, CDOMObject> { private final PlayerCharacterTrackingFacet trackingFacet = FacetLibrary .getFacet(PlayerCharacterTrackingFacet.class); private ClassFacet classFacet; private RaceFacet raceFacet; private TemplateFacet templateFacet; private LevelFacet levelFacet; private BonusCheckingFacet bonusCheckingFacet; /** * Roll the hitpoints for a single level. * * @param min the minimum number on the die * @param max the maximum number on the die * @param totalLevel the level the hitpoints are being rolled for (used in maths) * @return the hitpoints for the given level. */ private static int rollHP( final int min, final int max, final int totalLevel) { int roll; switch (SettingsHandler.getHPRollMethod()) { case Constants.HP_USER_ROLLED: roll = -1; break; case Constants.HP_AVERAGE: roll = max - min; // (n+1)/2 // average roll on a die with an odd # of sides works out exactly // average roll on a die with an even # of sides will have an extra 0.5 if (((totalLevel & 0x01) == 0) && ((roll & 0x01) != 0)) { ++roll; } roll = min + (roll / 2); break; case Constants.HP_AUTO_MAX: roll = max; break; case Constants.HP_PERCENTAGE: roll = (min - 1) + (int) ((SettingsHandler.getHPPercent() * ((max - min) + 1)) / 100.0); break; case Constants.HP_AVERAGE_ROUNDED_UP: roll = (int) Math.ceil((min + max) / 2.0); break; case Constants.HP_STANDARD:default: roll = Math.abs(RandomUtil.getRandomInt((max - min) + 1)) + min; break; } // if (SettingsHandler.getShowHPDialogAtLevelUp()) // { // final Object[] rollChoices = new Object[max - min + 2]; // rollChoices[0] = Constants.NONESELECTED; // // for (int i = min; i <= max; ++i) // { // rollChoices[i - min + 1] = i; // } // // while (min <= max) // { // //TODO: This must be refactored away. Core shouldn't know about gui. // final InputInterface ii = InputFactory.getInputInstance(); // final Object selectedValue = ii.showInputDialog(Globals.getRootFrame(), // "Randomly generate a number between " + min + " and " + max // + "." + Constants.LINE_SEPARATOR // + "Select it from the box below.", // SettingsHandler.getGame().getHPText() + " for " // + CoreUtility.ordinal(level) + " level of " + name, // MessageType.INFORMATION, // rollChoices, roll); // // if ((selectedValue != null) && (selectedValue instanceof Integer)) // { // roll = (Integer) selectedValue; // // break; // } // } // } return roll; } /** * Watches for new PCClassLevel objects to be granted to the Player * Character. When called, this then triggers the determination of the Hit * Points for that PCClassLevel. * * Triggered when one of the Facets to which FollowerOptionFacet listens * fires a DataFacetChangeEvent to indicate a FollowerOption was added to a * Player Character. * * @param dfce * The DataFacetChangeEvent containing the information about the * change * * @see pcgen.cdom.facet.event.DataFacetChangeListener#dataAdded(pcgen.cdom.facet.event.DataFacetChangeEvent) */ @Override public void dataAdded(DataFacetChangeEvent<CharID, CDOMObject> dfce) { CharID id = dfce.getCharID(); CDOMObject cdo = dfce.getCDOMObject(); PlayerCharacter pc = trackingFacet.getPC(id); if (!pc.isImporting()) { boolean first = true; for (PCClass pcClass : classFacet.getSet(id)) { // // Recalculate HPs in case HD have changed. // Processor<HitDie> dieLock = cdo.get(ObjectKey.HITDIE); if (dieLock != null) { for (int level = 1; level <= classFacet.getLevel(id, pcClass); level++) { HitDie baseHD = pcClass.getSafe(ObjectKey.LEVEL_HITDIE); if (!baseHD.equals(getLevelHitDie(id, pcClass, level))) { // If the HD has changed from base reroll rollHP(id, pcClass, level, first); pc.setDirty(true); } } } first = false; } } } @Override public void dataRemoved(DataFacetChangeEvent<CharID, CDOMObject> dfce) { /* * TODO This probably needs some form of symmetry - when a PCClassLevel * is removed, the number of hit points for that PCClassLevel is * removed. * * Alternatively, we can define this in such a way that the otherwise * lost information is saved, so that addition and removal of the same * level doesn't trigger new seleciton of hit points - need to define * the best strategy here (and just clearly document the decision) */ } /** * Returns the HitDie for the given PCClass and level in the Player * Character identified by the given CharID. * * @param id * The CharID identifying the Player Character for which the * HitDie of the given PCClass and level will be returned * @param pcClass * The PCClass for which the HitDie will be returned * @param classLevel * The level for which the HitDie will be returned * @return The HitDie for the given PCClass and level in the Player * Character identified by the given CharID */ public HitDie getLevelHitDie(CharID id, PCClass pcClass, int classLevel) { // Class Base Hit Die HitDie currDie = pcClass.getSafe(ObjectKey.LEVEL_HITDIE); Processor<HitDie> dieLock = raceFacet.get(id).get(ObjectKey.HITDIE); if (dieLock != null) { currDie = dieLock.applyProcessor(currDie, pcClass); } // Templates for (PCTemplate template : templateFacet.getSet(id)) { if (template != null) { Processor<HitDie> lock = template.get(ObjectKey.HITDIE); if (lock != null) { currDie = lock.applyProcessor(currDie, pcClass); } } } // Levels PCClassLevel cl = classFacet.getClassLevel(id, pcClass, classLevel); if (cl != null) { if (cl.get(ObjectKey.DONTADD_HITDIE) != null) { currDie = HitDie.ZERO; //null; } else { Processor<HitDie> lock = cl.get(ObjectKey.HITDIE); if (lock != null) { currDie = lock.applyProcessor(currDie, pcClass); } } } return currDie; } /** * Rolls the hit points for a given PCClass and level. * * @param id * The CharID identifying the Player Character on which the hit * points are to be rolled * @param pcc * The PCClass for which the hit points are to be rolled * @param level * The class level for which the hit points are to be rolled * @param first * And identifier indicating if this is the Player Character's * first level. */ public void rollHP(CharID id, PCClass pcc, int level, boolean first) { int roll = 0; HitDie lvlDie = getLevelHitDie(id, pcc, level); if ((lvlDie == null) || (lvlDie.getDie() == 0)) { roll = 0; } else { final int min = 1 + (int) bonusCheckingFacet.getBonus(id, "HD", "MIN") + (int) bonusCheckingFacet.getBonus(id, "HD", "MIN;CLASS." + pcc.getKeyName()); final int max = getLevelHitDie(id, pcc, level).getDie() + (int) bonusCheckingFacet.getBonus(id, "HD", "MAX") + (int) bonusCheckingFacet.getBonus(id, "HD", "MAX;CLASS." + pcc.getKeyName()); if (SettingsHandler.getGame().getHPFormula().isEmpty()) { if (first && level == 1 && SettingsHandler.isHPMaxAtFirstLevel() && (!SettingsHandler.isHPMaxAtFirstPCClassLevelOnly() || pcc .isType("PC"))) { roll = max; } else { PlayerCharacter pc = trackingFacet.getPC(id); if (!pc.isImporting()) { roll = rollHP(min, max, levelFacet.getTotalLevels(id)); } } } roll += ((int) bonusCheckingFacet.getBonus(id, "HP", "CURRENTMAXPERLEVEL")); } PCClassLevel classLevel = classFacet.getClassLevel(id, pcc, level - 1); set(id, classLevel, roll); } public void setClassFacet(ClassFacet classFacet) { this.classFacet = classFacet; } public void setRaceFacet(RaceFacet raceFacet) { this.raceFacet = raceFacet; } public void setTemplateFacet(TemplateFacet templateFacet) { this.templateFacet = templateFacet; } public void setLevelFacet(LevelFacet levelFacet) { this.levelFacet = levelFacet; } public void setBonusCheckingFacet(BonusCheckingFacet bonusCheckingFacet) { this.bonusCheckingFacet = bonusCheckingFacet; } /** * Initializes the connections for HitPointFacet to other facets. * * This method is automatically called by the Spring framework during * initialization of the HitPointFacet. */ public void init() { templateFacet.addDataFacetChangeListener(this); } }