/** *------------------------------------------------------------------------------ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.lostkingdomsfrontier.pfrpg.entity; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.lostkingdomsfrontier.pfrpg.AdjustableValue; import org.lostkingdomsfrontier.pfrpg.AdjustableValueListener; import org.lostkingdomsfrontier.pfrpg.Adjustment; import org.lostkingdomsfrontier.pfrpg.AdjustmentCategory; import org.lostkingdomsfrontier.pfrpg.AdjustmentListener; import org.lostkingdomsfrontier.pfrpg.ResourceException; import org.lostkingdomsfrontier.pfrpg.ResourcePool; /** * The HitPoints class manages the hit points for an entity. Hit points are managed as a * ResourcePool with support for an optional reserve. In general, Max hitPoints = base hp + (level * * conModifer). * * @author bebopjmm * */ public class HitPoints implements AdjustableValueListener { static final Log LOG = LogFactory.getLog(HitPoints.class); public enum LifeState { ALIVE, DYING, DEAD; public static LifeState assess(int poolCurrent, int deathThreshold) { if (poolCurrent > 0) return ALIVE; else if (poolCurrent <= deathThreshold) return DEAD; else return DYING; } }; private ResourcePool hp; private ResourcePool rp; private ConstitutionAdjustment abilityBonus; private AdjustableValue maxHP; private short level = 0; private short deathThreshold = 0; boolean hasReserve = false; /** * Constructs a new HitPoints object with the designated number of hit points and an optional * reserve. * * @param hitPoints number of hit points * @param hasReserve true if a reserve is to be established */ public HitPoints(short hitPoints, boolean hasReserve) { maxHP = new AdjustableValue(hitPoints); maxHP.setName("maxHP"); maxHP.subscribe(this); hp = new ResourcePool(hitPoints, true); hp.setDeficitAllowed(true); hp.setSurplusAllowed(true); this.hasReserve = hasReserve; if (this.hasReserve) { rp = new ResourcePool(hitPoints, true); } else { rp = new ResourcePool(0, true); } } /** * Constructs a new HitPoints object with 0 hit points. The Constitution AbilityValue is linked * to properly modify the hit points based on Constitution value and level. An optional reserve * is supported. * * @param conVal Constitution AbilityValue to influence hit points * @param hasReserve true if a reserve is to be established */ public HitPoints(AbilityValue conVal, boolean hasReserve) { hp = new ResourcePool(0, true); hp.setDeficitAllowed(true); hp.setSurplusAllowed(true); rp = new ResourcePool(0, true); abilityBonus = new ConstitutionAdjustment(conVal); maxHP = new AdjustableValue(0); maxHP.setName("maxHP"); maxHP.subscribe(this); maxHP.addAdjustment(abilityBonus); this.hasReserve = hasReserve; } /** * This method increased the hit point pool by increaseHP, and increments the level value used * in calculating the adjustment due to Constitution modifier. * * @param increaseHP number of hit points to be added due to level advancement */ public void advanceLevel(int increaseHP) { LOG.info("Advancing Level: increaseHP = " + increaseHP); level++; int oldBaseHP = maxHP.getBase(); maxHP.setBase(oldBaseHP + increaseHP); abilityBonus.setLevel(level); } /** * @param hasReserve */ public void setHasReserve(boolean hasReserve) { if (this.hasReserve == hasReserve) { return; } this.hasReserve = hasReserve; if (this.hasReserve) { // TODO fill rp to match current hp } else { // TODO empty rp } } /** * This method returns true if a reserve is in place, otherwise false. * * @return true if a reserve is in place, otherwise false. */ public boolean hasReserve() { return this.hasReserve; } /** * This method returns the current number of hit points. * * @return current number of hit points. */ public int getCurrentHP() { return hp.getPoolCurrent(); } /** * This method returns the maximum number of hit points. * * @return maximum number of hit points */ public int getMaxHP() { return hp.getPoolMax(); } /** * This method returns the current number of reserve hit points. * * @return current number of reserve hit points. */ public int getReserveHP() { return rp.getPoolCurrent(); } /** * @return the deathThreshold */ public int getDeathThreshold() { return deathThreshold; } /** * @param deathThreshold the deathThreshold to set */ public void setDeathThreshold(short deathThreshold) { if (deathThreshold > 0) { LOG.warn("Ignoring attempt to set death threshold > 0"); return; } this.deathThreshold = deathThreshold; } /** * This method inflicts the designated amount of hit point damage to the actor. * * @param hp amount of damage to inflict * @return LifeState following the damage. */ public LifeState damage(int hp) { synchronized (this.hp) { try { this.hp.expend(hp); return LifeState.assess(this.hp.getPoolCurrent(), this.deathThreshold); } catch (ResourceException ex) { return LifeState.DEAD; } } } /** * This method cures the designated amount of hit points to the actor. A DEAD actor cannot * receive healing. * * @param hp amount of curing to apply * @return LifeState following the curing */ public LifeState heal(short hp) { if (LifeState.assess(this.hp.getPoolCurrent(), deathThreshold) == LifeState.DEAD) { LOG.info("Cannot heal a dead creature"); return LifeState.DEAD; } synchronized (this.hp) { this.hp.replenish(hp); return LifeState.assess(this.hp.getPoolCurrent(), this.deathThreshold); } } /** * This method is triggered whenever maxHP is altered. It triggers the recalculation of pool * levels. * * @seeorg.rollinitiative.d20.AdjustableValueListener#valueChanged(org.rollinitiative.d20. * AdjustableValue * ) */ @Override public void valueChanged(AdjustableValue adjustable) { if (LOG.isDebugEnabled()) { LOG.debug("Recalculating hpPool due to change in maxHP."); } try { recalcMax(); } catch (ResourceException ex) { LOG.error("Failure to update the hp/rp pools!", ex); } } /** * This method updates the max pool sizes of hp and rp based on changes in the maxHP * AdjustableValue. */ protected void recalcMax() throws ResourceException { if (LOG.isDebugEnabled()) { LOG.debug("Assessing maxHP: New max=" + maxHP.getCurrent() + ", old max=" + hp.getPoolMax()); } int delta = maxHP.getCurrent() - hp.getPoolMax(); hp.updateMax(delta, true); if (hasReserve) { rp.updateMax(delta, true); } } /** * The ConstitutionAdjustment calculates the hp adjustment due to level and CON ability modifier * where adjustment = level * abilityMod * * @author bebopjmm * */ class ConstitutionAdjustment extends Adjustment implements AdjustmentListener { Adjustment conAdj_; public ConstitutionAdjustment(AbilityValue conVal) { super(AdjustmentCategory.INHERENT, (short)0, "hitPoints.CON"); conAdj_ = conVal.getModifier(); conAdj_.subscribe(this); this.setValue((short)(level * conAdj_.getValue())); } /** * This method assigns the value of the adjustment based on newLevel and the current CON * adjustment. It should trigger the valueChanged() method on subscribed maxHP * AdjustableValues. * * @param newLevel total levels for CON adjustment. */ public void setLevel(short newLevel) { this.setValue((short)(newLevel * conAdj_.getValue())); } /** * This will be invoked when the CON modifier changes. * * @see org.lostkingdomsfrontier.pfrpg.AdjustmentListener#valueChanged(org.lostkingdomsfrontier.pfrpg.Adjustment) */ @Override public void valueChanged(Adjustment adjustment) { LOG.debug("Updating CON adjustment to hp = level(" + level + ") * modifier(" + adjustment.getValue() + ")"); this.setValue((short)(level * adjustment.getValue())); } } }