/**
* CharacterAbilities.java
* Copyright James Dempsey, 2011
*
* 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
*
* Created on 19/03/2011 12:06:34 PM
*
* $Id$
*/
package pcgen.gui2.facade;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.SwingUtilities;
import pcgen.cdom.base.Category;
import pcgen.cdom.base.Constants;
import pcgen.cdom.content.CNAbility;
import pcgen.cdom.content.CNAbilityFactory;
import pcgen.cdom.enumeration.CharID;
import pcgen.cdom.enumeration.Nature;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.cdom.facet.FacetLibrary;
import pcgen.cdom.facet.GrantedAbilityFacet;
import pcgen.cdom.facet.event.DataFacetChangeEvent;
import pcgen.cdom.facet.event.DataFacetChangeListener;
import pcgen.cdom.helper.CNAbilitySelection;
import pcgen.core.Ability;
import pcgen.core.AbilityCategory;
import pcgen.core.AbilityUtilities;
import pcgen.core.Globals;
import pcgen.core.PlayerCharacter;
import pcgen.core.RuleConstants;
import pcgen.core.display.CharacterDisplay;
import pcgen.facade.core.AbilityCategoryFacade;
import pcgen.facade.core.AbilityFacade;
import pcgen.facade.core.DataSetFacade;
import pcgen.facade.core.UIDelegate;
import pcgen.facade.util.event.ChangeEvent;
import pcgen.facade.util.event.ChangeListener;
import pcgen.facade.util.DefaultListFacade;
import pcgen.facade.util.ListFacade;
import pcgen.core.utils.MessageType;
import pcgen.core.utils.ShowMessageDelegate;
import pcgen.system.LanguageBundle;
import pcgen.util.Logging;
import pcgen.util.enumeration.Tab;
import pcgen.util.enumeration.View;
/**
* The Class {@code CharacterAbilities} manages the interaction between
* the core and the user interface for abilities. It listens for changes in
* abilities in the core and then updates the lists provided to the UI to
* reflect the changes. The lists automatically notify any listeners in the
* UI of the changes.
*
* <br>
*
*
* @author James Dempsey <jdempsey@users.sourceforge.net>
*/
public class CharacterAbilities
{
private final PlayerCharacter theCharacter;
private final CharacterDisplay charDisplay;
private UIDelegate delegate;
private Map<AbilityCategoryFacade, DefaultListFacade<AbilityFacade>> abilityListMap;
private DefaultListFacade<AbilityCategoryFacade> activeCategories;
private CharID charID;
private DataSetFacade dataSetFacade;
private final List<ChangeListener> abilityCatSelectionListeners;
private final TodoManager todoManager;
private GrantedAbilityChangeHandler grantedAbilityChangeHandler;
/**
* Create a new instance of CharacterAbilities for a character.
* @param pc The character we are tracking abilities for.
* @param delegate The user interface delegate for notifying the user.
* @param dataSetFacade The datasets that the character is using.
* @param todoManager The user tasks tracker.
*/
public CharacterAbilities(PlayerCharacter pc, UIDelegate delegate,
DataSetFacade dataSetFacade, TodoManager todoManager)
{
theCharacter = pc;
charDisplay = pc.getDisplay();
this.delegate = delegate;
this.dataSetFacade = dataSetFacade;
this.todoManager = todoManager;
abilityCatSelectionListeners = new ArrayList<>();
initForCharacter();
}
/**
* Tidy up character listeners when closing the character.
*/
protected void closeCharacter()
{
GrantedAbilityFacet grantedAbilityFacet = FacetLibrary.getFacet(GrantedAbilityFacet.class);
grantedAbilityFacet.removeDataFacetChangeListener(grantedAbilityChangeHandler);
}
private void initForCharacter()
{
abilityListMap =
new LinkedHashMap<>();
activeCategories = new DefaultListFacade<>();
charID = theCharacter.getCharID();
GrantedAbilityFacet grantedAbilityFacet = FacetLibrary.getFacet(GrantedAbilityFacet.class);
//theCharacter.getAbilityList(cat, nature)
rebuildAbilityLists();
grantedAbilityChangeHandler = new GrantedAbilityChangeHandler();
grantedAbilityFacet.addDataFacetChangeListener(grantedAbilityChangeHandler);
}
void removeAbilityFromLists(AbilityCategory cat,
Ability ability, Nature nature)
{
removeCategorisedAbility(cat, ability, nature);
boolean stillActive = cat.isVisibleTo(View.VISIBLE_DISPLAY);
if (!stillActive && activeCategories.containsElement(cat))
{
activeCategories.removeElement(cat);
}
else
{
adviseSelectionChangeLater(cat);
}
updateAbilityCategoryLater(cat);
}
/**
* Rebuild the ability lists for the character to include the character's
* current abilities.
*/
synchronized void rebuildAbilityLists()
{
Map<AbilityCategoryFacade, DefaultListFacade<AbilityFacade>> workingAbilityListMap =
new LinkedHashMap<>();
DefaultListFacade<AbilityCategoryFacade> workingActiveCategories = new DefaultListFacade<>();
for (AbilityCategoryFacade category : dataSetFacade.getAbilities().getKeys())
{
AbilityCategory cat = (AbilityCategory) category;
for (CNAbility cna : theCharacter.getPoolAbilities(cat))
{
addCategorisedAbility(cna, workingAbilityListMap);
}
// deal with visibility
boolean visible = cat.isVisibleTo(theCharacter, View.VISIBLE_DISPLAY);
if (visible && !workingActiveCategories.containsElement(cat))
{
int index = getCatIndex(cat, workingActiveCategories);
workingActiveCategories.addElement(index, cat);
}
if (!visible && workingActiveCategories.containsElement(cat))
{
workingActiveCategories.removeElement(cat);
// updateAbilityCategoryTodo(cat);
}
if (visible)
{
adviseSelectionChangeLater(cat);
}
}
// Update map contents
for (AbilityCategoryFacade category : workingAbilityListMap.keySet())
{
DefaultListFacade<AbilityFacade> workingListFacade = workingAbilityListMap.get(category);
DefaultListFacade<AbilityFacade> masterListFacade = abilityListMap.get(category);
if (masterListFacade == null)
{
abilityListMap.put(category, workingListFacade);
}
else
{
masterListFacade.updateContentsNoOrder(workingListFacade.getContents());
}
updateAbilityCategoryTodo((AbilityCategory) category);
}
Set<AbilityCategoryFacade> origCats = new HashSet<>(abilityListMap.keySet());
for (AbilityCategoryFacade category : origCats)
{
if (!workingAbilityListMap.containsKey(category))
{
if (workingActiveCategories.containsElement(category))
{
abilityListMap.get(category).clearContents();
}
else
{
abilityListMap.remove(category);
}
updateAbilityCategoryTodo((AbilityCategory) category);
}
}
activeCategories.updateContents(workingActiveCategories.getContents());
}
private void updateAbilityCategoryTodo(Category<Ability> cat)
{
if (!(cat instanceof AbilityCategory))
{
return;
}
AbilityCategory category = (AbilityCategory) cat;
int numSelections = theCharacter.getAvailableAbilityPool(category).intValue();
if (category.getVisibility().isVisibleTo(View.HIDDEN_DISPLAY))
{
// Hide todos for categories that should not be displayed
numSelections = 0;
}
if (numSelections < 0)
{
todoManager.addTodo(new TodoFacadeImpl(
Tab.ABILITIES, category.getDisplayName(),
"in_featTodoTooMany", category.getType(), 1)); //$NON-NLS-1$
todoManager.removeTodo("in_featTodoRemain", category.getDisplayName()); //$NON-NLS-1$
}
else if (numSelections > 0)
{
todoManager.addTodo(new TodoFacadeImpl(
Tab.ABILITIES, category.getDisplayName(),
"in_featTodoRemain", category.getType(), 1)); //$NON-NLS-1$
todoManager.removeTodo("in_featTodoTooMany", category.getDisplayName()); //$NON-NLS-1$
}
else
{
todoManager.removeTodo("in_featTodoRemain", category.getDisplayName()); //$NON-NLS-1$
todoManager.removeTodo("in_featTodoTooMany", category.getDisplayName()); //$NON-NLS-1$
}
}
/**
* Determine where the ability category should be added to the active
* category list. This will keep the activate categories in the same sort
* order as the activity category list.
* @param abilityCategory The category being added
* @return The index at which to insert the category.
*/
private int getCatIndex(AbilityCategory abilityCategory, ListFacade<AbilityCategoryFacade> catList)
{
Set<AbilityCategoryFacade> allCategories = dataSetFacade.getAbilities().getKeys();
int index = 0;
for (AbilityCategoryFacade compCat : allCategories)
{
if (compCat == abilityCategory || index >= catList.getSize())
{
break;
}
if (catList.getElementAt(index) == compCat)
{
index++;
}
}
return index;
}
/**
* Add the ability to the categorised list held by CharacterAbilities.
* One copy will be added for each choice.
*
* @param cat The AbilityCategory that the ability is being added to.
* @param ability The ability being added.
* @param nature The nature via which the ability is being added.
* @param workingAbilityListMap The map to be adjusted.
*/
private void addCategorisedAbility(CNAbility cna, Map<AbilityCategoryFacade, DefaultListFacade<AbilityFacade>> workingAbilityListMap)
{
Ability ability = cna.getAbility();
List<CNAbilitySelection> cas = new ArrayList<>();
Category<Ability> cat = cna.getAbilityCategory();
Nature nature = cna.getNature();
if (ability.getSafe(ObjectKey.MULTIPLE_ALLOWED))
{
List<String> choices = theCharacter.getAssociationList(cna);
if (choices == null || choices.isEmpty())
{
Logging
.errorPrint("Ignoring Ability: "
+ ability
+ " ("
+ cat
+ " / "
+ nature
+ ") that UI has as added to the PC, but it has no associations");
}
else
{
for (String choice : choices)
{
cas.add(new CNAbilitySelection(CNAbilityFactory.getCNAbility(cat, nature, ability), choice));
}
}
}
else
{
cas.add(new CNAbilitySelection(CNAbilityFactory.getCNAbility(cat, nature, ability)));
}
for (CNAbilitySelection sel : cas)
{
addElement(workingAbilityListMap, sel);
}
}
private void removeCategorisedAbility(AbilityCategory cat,
Ability ability, Nature nature)
{
CNAbilitySelection cas;
if (ability.getSafe(ObjectKey.MULTIPLE_ALLOWED))
{
cas = new CNAbilitySelection(CNAbilityFactory.getCNAbility(cat, nature, ability), "");
}
else
{
cas = new CNAbilitySelection(CNAbilityFactory.getCNAbility(cat, nature, ability));
}
removeElement(cas);
}
/**
* Process a request by the user to add an ability. The user will be informed
* if the request cannot be allowed. Updates to the displayed lists are
* handled by events (see initForCharacter).
*
* @param categoryFacade The category in which the ability s bing added.
* @param abilityFacade The ability to be added.
*/
public void addAbility(AbilityCategoryFacade categoryFacade,
AbilityFacade abilityFacade)
{
if (abilityFacade == null || !(abilityFacade instanceof Ability)
|| categoryFacade == null
|| !(categoryFacade instanceof AbilityCategory))
{
return;
}
Ability ability = (Ability) abilityFacade;
AbilityCategory category = (AbilityCategory) categoryFacade;
if (!checkAbilityQualify(ability, category))
{
return;
}
// we can only be here if the PC can add the ability
try
{
theCharacter.setDirty(true);
theCharacter.getSpellList();
CNAbility cna = CNAbilityFactory.getCNAbility(category, Nature.NORMAL, ability);
AbilityUtilities.driveChooseAndAdd(cna, theCharacter, true);
}
catch (Exception exc)
{
Logging.errorPrint("Failed to add ability due to ", exc);
ShowMessageDelegate.showMessageDialog(LanguageBundle
.getFormattedString("in_iayAddAbility", exc.getMessage()), //$NON-NLS-1$
Constants.APPLICATION_NAME, MessageType.ERROR);
}
// Recalc the innate spell list
theCharacter.getSpellList();
theCharacter.calcActiveBonuses();
// update the ability info
rebuildAbilityLists();
}
/**
* Process a request by the user to remove an ability. The user will be
* informed if the request cannot be allowed. Updates to the displayed
* lists are handled by events (see initForCharacter).
*
* @param categoryFacade The category from which the ability is being removed.
* @param abilityFacade The ability to be removed.
*/
public void removeAbility(AbilityCategoryFacade categoryFacade,
AbilityFacade abilityFacade)
{
if (abilityFacade == null || !(abilityFacade instanceof Ability)
|| categoryFacade == null
|| !(categoryFacade instanceof AbilityCategory))
{
return;
}
Ability anAbility = (Ability) abilityFacade;
AbilityCategory theCategory = (AbilityCategory) categoryFacade;
try
{
Ability pcAbility =
theCharacter.getMatchingAbility(theCategory, anAbility,
Nature.NORMAL);
if (pcAbility != null)
{
CNAbility cna =
CNAbilityFactory.getCNAbility(theCategory,
Nature.NORMAL, anAbility);
AbilityUtilities.driveChooseAndAdd(cna, theCharacter, false);
theCharacter.adjustMoveRates();
}
}
catch (Exception exc)
{
Logging.errorPrintLocalised("in_iayFailedToRemoveAbility", exc); //$NON-NLS-1$
delegate.showErrorMessage(Constants.APPLICATION_NAME, LanguageBundle
.getString("in_iayRemoveAbility") //$NON-NLS-1$
+ ": " + exc.getMessage());
return;
}
theCharacter.calcActiveBonuses();
// update the ability info
rebuildAbilityLists();
return;
}
/**
* Retrieve the list of abilities for this category. The list
* will be updated when abilities are added and removed.
*
* @param category The ability category to be retrieved.
* @return The list of abilities.
*/
public ListFacade<AbilityFacade> getAbilities(AbilityCategoryFacade category)
{
DefaultListFacade<AbilityFacade> abList = abilityListMap.get(category);
if (abList == null)
{
abList = new DefaultListFacade<>();
abilityListMap.put(category, abList);
}
return abList;
}
/**
* @return The list of active ability categories.
*/
public ListFacade<AbilityCategoryFacade> getActiveAbilityCategories()
{
return activeCategories;
}
/**
* Get the total number of selections for this category.
* @param categoryFacade The ability category to be retrieved.
* @return The total number of choices.
*/
public int getTotalSelections(AbilityCategoryFacade categoryFacade)
{
if (categoryFacade == null
|| !(categoryFacade instanceof AbilityCategory))
{
return 0;
}
AbilityCategory category = (AbilityCategory) categoryFacade;
BigDecimal pool = theCharacter.getTotalAbilityPool(category);
return pool.intValue();
}
/**
* Get the number of selections that are remaining for this category.
* @param categoryFacade The ability category to be retrieved.
* @return The number of choices left.
*/
public int getRemainingSelections(AbilityCategoryFacade categoryFacade)
{
if (categoryFacade == null
|| !(categoryFacade instanceof AbilityCategory))
{
return 0;
}
AbilityCategory category = (AbilityCategory) categoryFacade;
BigDecimal pool = theCharacter.getAvailableAbilityPool(category);
return pool.intValue();
}
/**
* Set the number of selections that are remaining for this category.
* @param categoryFacade The ability category to be set.
* @param remaining The number of choices left.
*/
public void setRemainingSelection(AbilityCategoryFacade categoryFacade,
int remaining)
{
if (categoryFacade == null
|| !(categoryFacade instanceof AbilityCategory))
{
return;
}
AbilityCategory category = (AbilityCategory) categoryFacade;
BigDecimal pool = theCharacter.getAvailableAbilityPool(category);
final BigDecimal newRemain = new BigDecimal(remaining);
if (pool.equals(newRemain))
{
return;
}
theCharacter.adjustAbilities(category, newRemain
.subtract(pool));
}
/**
* Check if the character has an ability.
* @param category The ability category to be checked.
* @param ability The ability to be checked.
* @return true if the character has the ability, false otherwise.
*/
public boolean hasAbility(AbilityCategoryFacade category,
AbilityFacade ability)
{
DefaultListFacade<AbilityFacade> abList = abilityListMap.get(category);
if (abList == null)
{
return false;
}
return abList.containsElement(ability);
}
/**
* Register a listener to be advised of potential changes in the number of
* selections for an ability category.
* @param listener The class to be advised of a change.
*/
public void addAbilityCatSelectionListener(ChangeListener listener)
{
abilityCatSelectionListeners.add(listener);
}
/**
* Deregister a listener that should no longer be advised of potential changes
* in the number of selections for an ability category.
* @param listener The class to no longer be advised of a change.
*/
public void removeAbilityCatSelectionListener(ChangeListener listener)
{
abilityCatSelectionListeners.remove(listener);
}
/**
* Advise any listeners that the number of selections may have changed.
* @param cat The ability category that may have changed.
*/
private void fireAbilityCatSelectionUpdated(AbilityCategory cat)
{
ChangeEvent event = null;
for (ChangeListener listener : abilityCatSelectionListeners)
{
if (event == null)
{
event = new ChangeEvent(cat);
}
listener.ItemChanged(event);
}
}
/**
* After any other processing has finished, advise any listeners that
* the number of selections may have changed.
* @param cat The ability category that may have changed.
*/
private void adviseSelectionChangeLater(final AbilityCategory cat)
{
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
updateAbilityCategoryTodo(cat);
fireAbilityCatSelectionUpdated(cat);
refreshChoices(cat);
}
});
}
/**
* After any other processing has finished, refresh the todo information.
* This occurs as category totals are updated after we are notified of the
* abilities being added or removed.
* @param category The ability category that may have changed.
*/
private void updateAbilityCategoryLater(final Category<Ability> category)
{
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
updateAbilityCategoryTodo(category);
}
});
}
/**
* Signal that any ability that could have choices has been modified. This
* ensures that the choice display is up to date.
* @param category The ability category being refreshed.
*/
protected void refreshChoices(Category<Ability> category)
{
DefaultListFacade<AbilityFacade> listFacade = abilityListMap.get(category);
if (listFacade == null)
{
return;
}
for (AbilityFacade abilityFacade : listFacade)
{
Ability ability = (Ability) abilityFacade;
if (ability.getSafe(ObjectKey.MULTIPLE_ALLOWED))
{
listFacade.modifyElement(ability);
}
}
}
private boolean checkAbilityQualify(final Ability anAbility,
AbilityCategory theCategory)
{
final String aKey = anAbility.getKeyName();
boolean pcHasIt = theCharacter.hasAbilityKeyed(theCategory, aKey);
if (pcHasIt && !anAbility.getSafe(ObjectKey.MULTIPLE_ALLOWED))
{
delegate.showErrorMessage(Constants.APPLICATION_NAME, LanguageBundle
.getString("InfoAbility.Messages.Duplicate")); //$NON-NLS-1$
return false;
}
//TODO Why do we regrab the context-based Ability when an Ability was passed in?
Ability ability =
Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(
Ability.class, theCategory, aKey);
if (ability != null
&& !ability.qualifies(theCharacter, ability)
&& (!Globals.checkRule(RuleConstants.FEATPRE) || !AbilityUtilities
.isFeat(ability)))
{
delegate.showErrorMessage(Constants.APPLICATION_NAME, LanguageBundle
.getString("InfoAbility.Messages.NotQualified")); //$NON-NLS-1$
return false;
}
if ((ability != null))
{
final BigDecimal cost = ability.getSafe(ObjectKey.SELECTION_COST);
if (cost.compareTo(theCharacter
.getAvailableAbilityPool(theCategory)) > 0)
{
delegate.showErrorMessage(Constants.APPLICATION_NAME, LanguageBundle
.getString("InfoAbility.Messages.NoPoints")); //$NON-NLS-1$
return false;
}
}
return true;
}
private void addElement(Map<AbilityCategoryFacade, DefaultListFacade<AbilityFacade>> workingAbilityListMap, CNAbilitySelection cnas)
{
CNAbility cas = cnas.getCNAbility();
Ability ability = cas.getAbility();
if (!ability.getSafe(ObjectKey.VISIBILITY).isVisibleTo(View.VISIBLE_DISPLAY))
{
// Filter out hidden abilities
return;
}
AbilityCategoryFacade cat = (AbilityCategoryFacade) cas.getAbilityCategory();
DefaultListFacade<AbilityFacade> listFacade = workingAbilityListMap.get(cat);
if (listFacade == null)
{
listFacade = new DefaultListFacade<>();
workingAbilityListMap.put(cat, listFacade);
}
if (!listFacade.containsElement(ability))
{
listFacade.addElement(ability);
}
}
private void removeElement(CNAbilitySelection cnas)
{
CNAbility cas = cnas.getCNAbility();
Ability ability = cas.getAbility();
AbilityCategoryFacade cat = (AbilityCategoryFacade) cas.getAbilityCategory();
DefaultListFacade<AbilityFacade> listFacade = abilityListMap.get(cat);
if (listFacade != null)
{
listFacade.removeElement(ability);
}
}
/**
* The Class {@code GrantedAbilityChangeHandler} responds to changes to
* the character's list of granted abilities.
*/
private final class GrantedAbilityChangeHandler implements
DataFacetChangeListener<CharID, CNAbilitySelection>
{
@SuppressWarnings("nls")
@Override
public void dataAdded(DataFacetChangeEvent<CharID, CNAbilitySelection> dfce)
{
if (dfce.getCharID() != charID)
{
// Logging.debugPrint("CA for " + theCharacter.getName()
// + ". Ignoring granted ability added for character "
// + dfce.getCharID());
return;
}
if (Logging.isDebugMode())
{
Logging.debugPrint("Got granted ability added of "
+ dfce.getCDOMObject());
}
//Ability ability = dfce.getCDOMObject();
rebuildAbilityLists();
}
@SuppressWarnings("nls")
@Override
public void dataRemoved(DataFacetChangeEvent<CharID, CNAbilitySelection> dfce)
{
if (dfce.getCharID() != charID)
{
Logging
.debugPrint("CA for "
+ charDisplay.getName()
+ ". Ignoring granted ability removed for character "
+ dfce.getCharID());
return;
}
if (Logging.isDebugMode())
{
Logging.debugPrint("Got granted ability removed of "
+ dfce.getCDOMObject());
}
//Ability ability = dfce.getCDOMObject();
rebuildAbilityLists();
}
}
}