/* * Copyright (c) Thomas Parker, 2009. * * 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.analysis; import java.util.EventListener; import java.util.EventObject; import javax.swing.event.EventListenerList; import pcgen.base.formula.Formula; import pcgen.cdom.enumeration.CharID; import pcgen.cdom.enumeration.FormulaKey; import pcgen.cdom.facet.FormulaResolvingFacet; import pcgen.cdom.facet.base.AbstractStorageFacet; import pcgen.cdom.facet.model.RaceFacet; import pcgen.cdom.facet.model.TemplateFacet; import pcgen.cdom.facet.model.ClassFacet.ClassLevelChangeEvent; import pcgen.cdom.facet.model.ClassFacet.ClassLevelChangeListener; import pcgen.cdom.facet.model.ClassFacet.ClassLevelObjectChangeEvent; import pcgen.core.PCTemplate; import pcgen.core.Race; /** * LevelFacet stores information about the Level of a Player Character. This * includes the ability to distinguish what is a Monster Level, PC level, NPC * level, etc. * * @author Thomas Parker (thpr [at] yahoo.com) */ public class LevelFacet extends AbstractStorageFacet<CharID> implements ClassLevelChangeListener { private TemplateFacet templateFacet; private RaceFacet raceFacet; private FormulaResolvingFacet formulaResolvingFacet; private final LevelChangeSupport support = new LevelChangeSupport(); /** * Returns the non-Monster level count for the Player Character identified * by the given CharID. * * @param id * The CharID of the Player Character for which the non-Monster * level count will be returned * @return The non-Monster level count for the Player Character identified * by the given CharID */ public int getNonMonsterLevelCount(CharID id) { LevelCacheInfo info = getInfo(id); return info == null ? 0 : info.nonMonsterLevels; } /** * Returns the Monster level count for the Player Character identified by * the given CharID. * * @param id * The CharID of the Player Character for which the Monster level * count will be returned * @return The Monster level count for the Player Character identified by * the given CharID */ public int getMonsterLevelCount(CharID id) { LevelCacheInfo info = getInfo(id); return info == null ? 0 : info.monsterLevels; } /** * Returns the level adjustment for the Player Character identified by the * given CharID. * * @param id * The CharID of the Player Character for which the level * adjustment will be returned * @return The level adjustment for the Player Character identified by the * given CharID */ public int getLevelAdjustment(CharID id) { Race race = raceFacet.get(id); int levelAdj = 0; if (race != null) { Formula raceLA = race.getSafe(FormulaKey.LEVEL_ADJUSTMENT); levelAdj += formulaResolvingFacet.resolve(id, raceLA, "").intValue(); } for (PCTemplate template : templateFacet.getSet(id)) { Formula templateLA = template.getSafe(FormulaKey.LEVEL_ADJUSTMENT); levelAdj += formulaResolvingFacet.resolve(id, templateLA, "").intValue(); } return levelAdj; } /** * Returns the Effective Character Level (ECL) for the Player Character * identified by the given CharID. * * @param id * The CharID identifying the Player Character for which the * Effective Character Level will be returned * @return The Effective Character Level (ECL) for the Player Character * identified by the given CharID */ public int getECL(CharID id) { int levelAdjustment = getLevelAdjustment(id); LevelCacheInfo info = getInfo(id); if (info == null) { return levelAdjustment; } return info.nonMonsterLevels + info.monsterLevels + levelAdjustment; } /** * Returns the total levels for the Player Character identified by the given * CharID. * * @param id * The CharID identifying the Player Character for which the * total levels will be returned * @return The total levels for the Player Character identified by the given * CharID * */ public int getTotalLevels(CharID id) { LevelCacheInfo info = getInfo(id); // Monster hit dice count towards total levels // sage_sam changed 03 Dec 2002 for Bug #646816 return info == null ? 0 : info.nonMonsterLevels + info.monsterLevels; } /** * Returns the LevelCacheInfofor this LevelFacet and the given CharID. Will * return a new, empty LevelCacheInfo if no information has been set in this * LevelFacet for the given CharID. Will not return null. * * Note that this method SHOULD NOT be public. The LevelCacheInfo object is * owned by LevelFacet, and since it can be modified, a reference to that * object should not be exposed to any object other than LevelFacet. * * @param id * The CharID for which the Map should be returned * @return The LevelCacheInfo for the Player Character represented by the * given CharID. */ private LevelCacheInfo getConstructingInfo(CharID id) { LevelCacheInfo lci = getInfo(id); if (lci == null) { lci = new LevelCacheInfo(); setCache(id, lci); } return lci; } /** * Returns the LevelCacheInfofor this LevelFacet and the given CharID. Will * return null if no information has been set in this LevelFacet for the * given CharID. * * Note that this method SHOULD NOT be public. The LevelCacheInfo object is * owned by LevelFacet, and since it can be modified, a reference to that * object should not be exposed to any object other than LevelFacet. * * @param id * The CharID for which the Map should be returned * @return The LevelCacheInfo for the Player Character represented by the * given CharID. */ private LevelCacheInfo getInfo(CharID id) { return (LevelCacheInfo) getCache(id); } /** * Data structure for caching level information about a Player Character * * @author Thomas Parker (thpr [at] yahoo.com) */ private static class LevelCacheInfo { public int monsterLevels; public int nonMonsterLevels; @Override public int hashCode() { return monsterLevels * nonMonsterLevels; } @Override public boolean equals(Object o) { if (o instanceof LevelCacheInfo) { LevelCacheInfo other = (LevelCacheInfo) o; return monsterLevels == other.monsterLevels && nonMonsterLevels == other.nonMonsterLevels; } return false; } } @Override public void levelChanged(ClassLevelChangeEvent lce) { CharID id = lce.getCharID(); LevelCacheInfo lci = getConstructingInfo(id); int levelChange = lce.getNewLevel() - lce.getOldLevel(); if (levelChange != 0) { if (lce.getPCClass().isMonster()) { lci.monsterLevels += levelChange; } else { lci.nonMonsterLevels += levelChange; } support.fireLevelChangeEvent(id); } } @Override public void levelObjectChanged(ClassLevelObjectChangeEvent lce) { //ignore } /** * Adds a LevelChangeListener to receive LevelChangeEvents from LevelFacet. * * Note that the LevelChangeListeners are a list, meaning a given * LevelChangeListener can be added more than once, and if that occurs, it * must be removed an equivalent number of times in order to no longer * receive events from this LevelFacet. * * @param listener * The LevelChangeListener to receive LevelChangeEvents from this * LevelFacet */ public void addLevelChangeListener(LevelChangeListener listener) { support.addLevelChangeListener(listener); } /** * Removes a LevelChangeListener so that it does not receive * LevelChangeEvents from LevelFacet. * * Note that the LevelChangeListeners are a list, meaning a given * LevelChangeListener can be added more than once, and if that occurs, it * must be removed an equivalent number of times in order to no longer * receive events from this LevelFacet. * * @param listener * The LevelChangeListener to no longer receive LevelChangeEvents * from this LevelFacet */ public void removeLevelChangeListener(LevelChangeListener listener) { support.removeLevelChangeListener(listener); } /** * Interface for a LevelChangeListener that wants to receive * LevelChangeEvents from LevelFacet. * * @author Thomas Parker (thpr [at] yahoo.com) */ public static interface LevelChangeListener extends EventListener { public void levelChanged(LevelChangeEvent lce); } public static class LevelChangeEvent extends EventObject { /** * The ID indicating the owning character for this DataFacetChangeEvent */ private final CharID charID; public LevelChangeEvent(CharID source) { super(source); charID = source; } /** * Returns an identifier indicating the PlayerCharacter on which this * event occurred. * * @return A identifier indicating the PlayerCharacter on which this * event occurred. */ public CharID getCharID() { return charID; } } public static class LevelChangeSupport { /** * The listeners to which LevelChangeEvents will be fired when a change * in the source DataFacet occurs. */ private final EventListenerList listenerList = new EventListenerList(); /** * Adds a new LevelChangeListener to receive LevelChangeEvents from the * source DataFacet. * * @param listener * The LevelChangeListener to receive LevelChangeEvents */ public void addLevelChangeListener(LevelChangeListener listener) { listenerList.add(LevelChangeListener.class, listener); } /** * Returns an Array of LevelChangeListeners receiving LevelChangeEvents * from the source DataFacet. * * Ownership of the returned Array is transferred to the calling Object. * No reference to the Array is maintained by LevelChangeSupport. * However, the LevelChangeListeners contained in the Array are * (obviously!) returned BY REFERENCE, and care should be taken with * modifying those LevelChangeListeners.* * * @return An Array of LevelChangeListeners receiving LevelChangeEvents * from the source DataFacet */ public synchronized LevelChangeListener[] getLevelChangeListeners() { return listenerList.getListeners(LevelChangeListener.class); } /** * Removes a LevelChangeListener so that it will no longer receive * LevelChangeEvents from the source DataFacet. * * @param listener * The LevelChangeListener to be removed */ public void removeLevelChangeListener(LevelChangeListener listener) { listenerList.remove(LevelChangeListener.class, listener); } /** * Sends a LevelChangeEvent to the LevelChangeListeners that are * receiving LevelChangeEvents from the source DataFacet. * * @param id * An identifier indicating the Player Character on which a * level change has occurred */ protected void fireLevelChangeEvent(CharID id) { LevelChangeListener[] listeners = listenerList .getListeners(LevelChangeListener.class); /* * This list is decremented from the end of the list to the * beginning in order to maintain consistent operation with how Java * AWT and Swing listeners are notified of Events (they are in * reverse order to how they were added to the Event-owning object). */ LevelChangeEvent ccEvent = null; for (int i = listeners.length - 1; i >= 0; i--) { // Lazily create event if (ccEvent == null) { ccEvent = new LevelChangeEvent(id); } listeners[i].levelChanged(ccEvent); } } } /** * @param templateFacet * the templateFacet to set */ public void setTemplateFacet(TemplateFacet templateFacet) { this.templateFacet = templateFacet; } /** * @param raceFacet * the raceFacet to set */ public void setRaceFacet(RaceFacet raceFacet) { this.raceFacet = raceFacet; } /** * @param resolveFacet * the resolveFacet to set */ public void setFormulaResolvingFacet(FormulaResolvingFacet resolveFacet) { this.formulaResolvingFacet = resolveFacet; } /** * Copies the contents of the LevelFacet from one Player Character to * another Player Character, based on the given CharIDs representing those * Player Characters. * * This is a method in LevelFacet in order to avoid exposing the mutable * LevelCacheInfo object to other classes. This should not be inlined, as * the LevelCacheInfo is internal information to LevelFacet and should not * be exposed to other classes. * * Note also the copy is a one-time event and no references are maintained * between the Player Characters represented by the given CharIDs (meaning * once this copy takes place, any change to the LevelFacet of one Player * Character will only impact the Player Character where the LevelFacet was * changed). * * @param source * The CharID representing the Player Character from which the * information should be copied * @param copy * The CharID representing the Player Character to which the * information should be copied */ @Override public void copyContents(CharID source, CharID copy) { LevelCacheInfo info = getInfo(source); if (info != null) { LevelCacheInfo copyinfo = getConstructingInfo(copy); copyinfo.monsterLevels = info.monsterLevels; copyinfo.nonMonsterLevels = info.nonMonsterLevels; } } }