/**
* SpellSupportFacadeImpl.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 15/10/2011 4:20:41 PM
*
* $Id$
*/
package pcgen.gui2.facade;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import org.apache.commons.lang3.StringUtils;
import pcgen.base.lang.StringUtil;
import pcgen.base.util.DoubleKeyMapToList;
import pcgen.base.util.HashMapToList;
import pcgen.cdom.base.CDOMList;
import pcgen.cdom.base.CDOMObject;
import pcgen.cdom.base.Constants;
import pcgen.cdom.content.CNAbility;
import pcgen.cdom.enumeration.AssociationKey;
import pcgen.cdom.enumeration.FactKey;
import pcgen.cdom.enumeration.IntegerKey;
import pcgen.cdom.enumeration.ListKey;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.cdom.enumeration.SourceFormat;
import pcgen.core.Ability;
import pcgen.core.AbilityCategory;
import pcgen.core.Domain;
import pcgen.core.Equipment;
import pcgen.core.Globals;
import pcgen.core.PCClass;
import pcgen.core.PObject;
import pcgen.core.PlayerCharacter;
import pcgen.core.Race;
import pcgen.core.SettingsHandler;
import pcgen.core.SpellProhibitor;
import pcgen.core.SpellSupportForPCClass;
import pcgen.core.analysis.OutputNameFormatting;
import pcgen.core.character.CharacterSpell;
import pcgen.core.character.SpellBook;
import pcgen.core.character.SpellInfo;
import pcgen.core.display.CharacterDisplay;
import pcgen.core.spell.Spell;
import pcgen.core.utils.MessageType;
import pcgen.core.utils.ShowMessageDelegate;
import pcgen.facade.core.ChooserFacade.ChooserTreeViewType;
import pcgen.facade.core.ClassFacade;
import pcgen.facade.core.DataSetFacade;
import pcgen.facade.core.EquipmentFacade;
import pcgen.facade.core.EquipmentListFacade.EquipmentListEvent;
import pcgen.facade.core.EquipmentListFacade.EquipmentListListener;
import pcgen.facade.core.InfoFacade;
import pcgen.facade.core.InfoFactory;
import pcgen.facade.core.SpellFacade;
import pcgen.facade.core.SpellSupportFacade;
import pcgen.facade.core.UIDelegate;
import pcgen.facade.util.DefaultListFacade;
import pcgen.facade.util.DefaultReferenceFacade;
import pcgen.facade.util.ListFacade;
import pcgen.facade.util.event.ListEvent;
import pcgen.facade.util.event.ListListener;
import pcgen.gui2.tools.Utility;
import pcgen.gui2.util.HtmlInfoBuilder;
import pcgen.io.ExportUtilities;
import pcgen.system.BatchExporter;
import pcgen.system.LanguageBundle;
import pcgen.system.PCGenSettings;
import pcgen.util.Logging;
import pcgen.util.enumeration.Tab;
import pcgen.util.enumeration.View;
/**
* The Class {@code SpellSupportFacadeImpl} marshals the spell data for a
* character for display in the user interface. It also responds to any actions
* by the UI layer on the character's spells.
*
* <br>
*
* @author James Dempsey <jdempsey@users.sourceforge.net>
*/
public class SpellSupportFacadeImpl implements SpellSupportFacade,
EquipmentListListener, ListListener<EquipmentFacade>
{
private final PlayerCharacter pc;
private final CharacterDisplay charDisplay;
private UIDelegate delegate;
private DefaultListFacade<SpellNode> availableSpellNodes;
private DefaultListFacade<SpellNode> allKnownSpellNodes;
private DefaultListFacade<SpellNode> knownSpellNodes;
private DefaultListFacade<SpellNode> preparedSpellNodes;
private DefaultListFacade<SpellNode> bookSpellNodes;
private List<SpellNode> preparedSpellLists;
private List<SpellNode> spellBooks;
private Map<String, RootNodeImpl> rootNodeMap;
private final DataSetFacade dataSet;
private DefaultListFacade<String> spellBookNames;
private DefaultReferenceFacade<String> defaultSpellBook;
private TodoManager todoManager;
private CharacterFacadeImpl pcFacade;
private final InfoFactory infoFactory;
/**
* Create a new instance of SpellSupportFacadeImpl to manage the display and update of a
* character's spells.
*
* @param pc The character we are managing.
* @param delegate The delegate class for UI display.
* @param dataSet The current data being used.
* @param todoManager The user tasks tracker.
* @param pcFacade The character facade.
*/
public SpellSupportFacadeImpl(PlayerCharacter pc, UIDelegate delegate,
DataSetFacade dataSet, TodoManager todoManager,
CharacterFacadeImpl pcFacade)
{
this.pc = pc;
this.infoFactory = pcFacade.getInfoFactory();
this.charDisplay = pc.getDisplay();
this.delegate = delegate;
this.dataSet = dataSet;
this.todoManager = todoManager;
this.pcFacade = pcFacade;
rootNodeMap = new HashMap<>();
spellBookNames = new DefaultListFacade<>();
defaultSpellBook = new DefaultReferenceFacade<>(charDisplay.getSpellBookNameToAutoAddKnown());
availableSpellNodes = new DefaultListFacade<>();
buildAvailableNodes();
allKnownSpellNodes = new DefaultListFacade<>();
knownSpellNodes = new DefaultListFacade<>();
preparedSpellNodes = new DefaultListFacade<>();
bookSpellNodes = new DefaultListFacade<>();
preparedSpellLists = new ArrayList<>();
spellBooks = new ArrayList<>();
buildKnownPreparedNodes();
updateSpellsTodo();
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#getAvailableSpellNodes()
*/
@Override
public ListFacade<SpellNode> getAvailableSpellNodes()
{
return availableSpellNodes;
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#getAllKnownSpellNodes()
*/
@Override
public ListFacade<SpellNode> getAllKnownSpellNodes()
{
return allKnownSpellNodes;
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#getKnownSpellNodes()
*/
@Override
public ListFacade<SpellNode> getKnownSpellNodes()
{
return knownSpellNodes;
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#getPreparedSpellNodes()
*/
@Override
public ListFacade<SpellNode> getPreparedSpellNodes()
{
return preparedSpellNodes;
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#getBookSpellNodes()
*/
@Override
public ListFacade<SpellNode> getBookSpellNodes()
{
return bookSpellNodes;
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#addKnownSpell(pcgen.core.facade.SpellSupportFacade.SpellNode)
*/
@Override
public void addKnownSpell(SpellNode spell)
{
SpellNode node =
addSpellToCharacter(spell, Globals.getDefaultSpellBook(), new ArrayList<>());
if (node != null)
{
allKnownSpellNodes.addElement(node);
knownSpellNodes.addElement(node);
if (!StringUtils.isEmpty(charDisplay.getSpellBookNameToAutoAddKnown()))
{
addToSpellBook(node, charDisplay.getSpellBookNameToAutoAddKnown());
}
}
updateSpellsTodo();
pcFacade.refreshAvailableTempBonuses();
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#removeKnownSpell(pcgen.core.facade.SpellSupportFacade.SpellNode)
*/
@Override
public void removeKnownSpell(SpellNode spell)
{
//TODO: This should also remove the spell from books and lists
if (removeSpellFromCharacter(spell, Globals.getDefaultSpellBook()))
{
allKnownSpellNodes.removeElement(spell);
knownSpellNodes.removeElement(spell);
}
updateSpellsTodo();
pcFacade.refreshAvailableTempBonuses();
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#addPreparedSpell(pcgen.core.facade.SpellSupportFacade.SpellNode, java.lang.String)
*/
@Override
public void addPreparedSpell(SpellNode spell, String spellList, boolean useMetamagic)
{
List<Ability> metamagicFeats = new ArrayList<>();
if (useMetamagic)
{
metamagicFeats = queryUserForMetamagic(spell);
if (metamagicFeats == null)
{
return;
}
}
SpellNode node =
addSpellToCharacter(spell, spellList, metamagicFeats);
if (node != null)
{
if (preparedSpellNodes.containsElement(node))
{
SpellNode spellNode = preparedSpellNodes.getElementAt(preparedSpellNodes.getIndexOfElement(node));
spellNode.addCount(1);
// Remove and readd to ensure the display is updated
preparedSpellNodes.removeElement(spellNode);
preparedSpellNodes.addElement(spellNode);
}
else
{
preparedSpellNodes.addElement(node);
}
// Remove dummy spelllist node from the list
for (Iterator<SpellNode> iterator = preparedSpellNodes.iterator(); iterator.hasNext();)
{
SpellNode sn = iterator.next();
if (sn.getSpell() == null && spellList.equals(sn.getRootNode().getName()))
{
iterator.remove();
}
}
}
}
private void updateSpellsTodo()
{
boolean hasFree = false;
for (PCClass aClass : charDisplay.getClassSet())
{
if (pc.getSpellSupport(aClass).hasKnownList() || pc.getSpellSupport(aClass).hasKnownSpells(pc))
{
int highestSpellLevel = pc.getSpellSupport(aClass).getHighestLevelSpell(pc);
for (int i = 0; i <= highestSpellLevel; ++i)
{
if (pc.availableSpells(i, aClass,
Globals.getDefaultSpellBook(), true, false)
|| pc.availableSpells(i, aClass,
Globals.getDefaultSpellBook(), true, true))
{
hasFree = true;
break;
}
}
}
}
if (hasFree)
{
todoManager.addTodo(new TodoFacadeImpl(Tab.SPELLS, "Known",
"in_splTodoRemain", 120));
}
else
{
todoManager.removeTodo("in_splTodoRemain");
}
}
/**
* Request the metamagic feats to be applied to a spell from the user via
* a chooser.
*
* @param spellNode The spell to have metamagic applied
* @return The list of metamagic feats to be applied.
*/
private List<Ability> queryUserForMetamagic(SpellNode spellNode)
{
// get the list of metamagic feats for the PC
List<InfoFacade> availableList =
buildAvailableMetamagicFeatList(spellNode);
if (availableList.isEmpty())
{
return Collections.emptyList();
}
String label = dataSet.getGameMode().getAddWithMetamagicMessage();
if (StringUtils.isEmpty(label))
{
label = LanguageBundle.getString("InfoSpells.add.with.metamagic");
}
final ArrayList<Ability> selectedList = new ArrayList<>();
GeneralChooserFacadeBase chooserFacade =
new GeneralChooserFacadeBase(label, availableList,
new ArrayList<>(), 99, infoFactory)
{
@Override
public void commit()
{
for (InfoFacade item : getSelectedList())
{
selectedList.add((Ability) item);
}
}
};
chooserFacade.setDefaultView(ChooserTreeViewType.NAME);
boolean result = delegate.showGeneralChooser(chooserFacade);
return result ? selectedList : null;
}
/**
* Get the list of metatmagic feats that the character can use.
* @param spellNode The spell the feats would be applied to.
* @return The list of metamagic feats.
*/
private List<InfoFacade> buildAvailableMetamagicFeatList(SpellNode spellNode)
{
List<Ability> characterMetaMagicFeats = new ArrayList<>();
List<CNAbility> feats = pc.getCNAbilities(AbilityCategory.FEAT);
for (CNAbility cna : feats)
{
Ability aFeat = cna.getAbility();
if (aFeat.isType("Metamagic") //$NON-NLS-1$
&& !aFeat.getSafe(ObjectKey.VISIBILITY).isVisibleTo(View.HIDDEN_EXPORT))
{
characterMetaMagicFeats.add(aFeat);
}
}
Globals.sortPObjectListByName(characterMetaMagicFeats);
if (!(spellNode.getSpell() instanceof SpellFacadeImplem))
{
return Collections.emptyList();
}
List<InfoFacade> availableList = new ArrayList<>();
availableList.addAll(characterMetaMagicFeats);
return availableList;
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#removePreparedSpell(pcgen.core.facade.SpellSupportFacade.SpellNode, java.lang.String)
*/
@Override
public void removePreparedSpell(SpellNode spell, String spellList)
{
if (removeSpellFromCharacter(spell, spellList))
{
if (spell.getCount() > 1)
{
spell.addCount(-1);
// Remove and readd to ensure the display is updated
preparedSpellNodes.removeElement(spell);
preparedSpellNodes.addElement(spell);
}
else
{
preparedSpellNodes.removeElement(spell);
}
addDummyNodeIfSpellListEmpty(spellList);
}
}
/**
* If there are no spells in the spell list, add in the spell list
* placeholder node so that the list shows in the UI.
*
* @param spellList The list to be checked.
*/
private void addDummyNodeIfSpellListEmpty(String spellList)
{
boolean spellListEmpty = true;
for (SpellNode node : preparedSpellNodes)
{
if (spellList.equals(node.getRootNode().getName()))
{
spellListEmpty = false;
break;
}
}
if (spellListEmpty)
{
for (SpellNode listNode : preparedSpellLists)
{
if (spellList.equals(listNode.getRootNode().getName()))
{
preparedSpellNodes.addElement(listNode);
}
}
}
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#addSpellList(java.lang.String)
*/
@Override
public void addSpellList(String spellList)
{
if (StringUtils.isEmpty(spellList))
{
return;
}
// Prevent spellbooks being given the same name as a class
for (PCClass current : Globals.getContext().getReferenceContext().getConstructedCDOMObjects(PCClass.class))
{
if ((spellList.equals(current.getKeyName())))
{
JOptionPane.showMessageDialog(null, LanguageBundle
.getString("in_spellbook_name_error"), //$NON-NLS-1$
Constants.APPLICATION_NAME, JOptionPane.ERROR_MESSAGE);
return;
}
}
if (pc.addSpellBook(spellList))
{
pc.setDirty(true);
DummySpellNodeImpl spellListNode =
new DummySpellNodeImpl(getRootNode(spellList));
preparedSpellLists.add(spellListNode);
addDummyNodeIfSpellListEmpty(spellList);
}
else
{
JOptionPane.showMessageDialog(null, LanguageBundle
.getFormattedString(
"InfoPreparedSpells.add.list.fail", spellList), //$NON-NLS-1$
Constants.APPLICATION_NAME, JOptionPane.ERROR_MESSAGE);
return;
}
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#removeSpellList(java.lang.String)
*/
@Override
public void removeSpellList(String spellList)
{
if (spellList.equalsIgnoreCase(Globals.getDefaultSpellBook()))
{
Logging.errorPrint(LanguageBundle
.getString("InfoSpells.can.not.delete.default.spellbook")); //$NON-NLS-1$
return;
}
if (pc.delSpellBook(spellList))
{
pc.setDirty(true);
for (Iterator<SpellNode> iterator = preparedSpellLists.iterator(); iterator.hasNext();)
{
SpellNode listNode = iterator.next();
if (spellList.equals(listNode.getRootNode().getName()))
{
iterator.remove();
}
}
for (Iterator<SpellNode> iterator = preparedSpellNodes.iterator(); iterator.hasNext();)
{
SpellNode spell = iterator.next();
if (spellList.equals(spell.getRootNode().getName()))
{
iterator.remove();
}
}
}
else
{
Logging.errorPrint("delBookButton:failed "); //$NON-NLS-1$
return;
}
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#addToSpellBook(pcgen.core.facade.SpellSupportFacade.SpellNode, java.lang.String)
*/
@Override
public void addToSpellBook(SpellNode spell, String spellBook)
{
String bookName = spellBook;
if (bookName.endsWith("]") && bookName.contains(" ["))
{
bookName = bookName.substring(0, bookName.lastIndexOf(" ["));
}
SpellNode node =
addSpellToCharacter(spell, bookName, new ArrayList<>());
if (node != null)
{
if (bookSpellNodes.containsElement(node))
{
SpellNode spellNode = bookSpellNodes.getElementAt(bookSpellNodes.getIndexOfElement(node));
spellNode.addCount(1);
// Remove and readd to ensure the display is updated
bookSpellNodes.removeElement(spellNode);
bookSpellNodes.addElement(spellNode);
}
else
{
bookSpellNodes.addElement(node);
}
// Remove dummy spellbook node from the list
for (Iterator<SpellNode> iterator = bookSpellNodes.iterator(); iterator.hasNext();)
{
SpellNode sn = iterator.next();
if (sn.getSpell() == null && bookName.equals(sn.getRootNode().getName()))
{
iterator.remove();
}
}
}
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#removeFromSpellBook(pcgen.core.facade.SpellSupportFacade.SpellNode, java.lang.String)
*/
@Override
public void removeFromSpellBook(SpellNode spell, String spellBook)
{
if (removeSpellFromCharacter(spell, spellBook))
{
if (spell.getCount() > 1)
{
spell.addCount(-1);
// Remove and readd to ensure the display is updated
bookSpellNodes.removeElement(spell);
bookSpellNodes.addElement(spell);
}
else
{
bookSpellNodes.removeElement(spell);
}
addDummyNodeIfSpellBookEmpty(spellBook);
}
}
/**
* If there are no spells in the spell book, add in the spell book
* placeholder node so that the book shows in the UI.
*
* @param spellBook The book to be checked.
*/
private void addDummyNodeIfSpellBookEmpty(String spellBook)
{
boolean spellListEmpty = true;
for (SpellNode node : bookSpellNodes)
{
if (spellBook.equals(node.getRootNode().getName()))
{
spellListEmpty = false;
break;
}
}
if (spellListEmpty)
{
for (SpellNode listNode : spellBooks)
{
if (spellBook.equals(listNode.getRootNode().getName()))
{
bookSpellNodes.addElement(listNode);
}
}
}
}
@Override
public void refreshAvailableKnownSpells()
{
buildAvailableNodes();
buildKnownPreparedNodes();
updateSpellsTodo();
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade#getClassInfo(pcgen.core.facade.ClassFacade)
*/
@Override
public String getClassInfo(ClassFacade spellcaster)
{
if (!(spellcaster instanceof PCClass))
{
return "";
}
PCClass aClass = (PCClass) spellcaster;
SpellSupportForPCClass spellSupport = pc.getSpellSupport(aClass);
int highestSpellLevel = spellSupport.getHighestLevelSpell(pc);
final HtmlInfoBuilder b = new HtmlInfoBuilder();
b.append("<table border=1><tr><td><font size=-2><b>"); //$NON-NLS-1$
b.append(OutputNameFormatting.piString(aClass, false)).append(" ["); //$NON-NLS-1$
b.append(String.valueOf(charDisplay.getLevel(aClass)
+ (int) pc.getTotalBonusTo("PCLEVEL", aClass.getKeyName()))); //$NON-NLS-1$
b.append("]</b></font></td>"); //$NON-NLS-1$
for (int i = 0; i <= highestSpellLevel; ++i)
{
b.append("<td><font size=-2><b><center> "); //$NON-NLS-1$
b.append(String.valueOf(i));
b.append(" </b></center></font></td>"); //$NON-NLS-1$
}
b.append("</tr>"); //$NON-NLS-1$
b.append("<tr><td><font size=-1><b>Cast</b></font></td>"); //$NON-NLS-1$
for (int i = 0; i <= highestSpellLevel; ++i)
{
b.append("<td><font size=-1><center>"); //$NON-NLS-1$
b.append(getNumCast(aClass, i, pc));
b.append("</center></font></td>"); //$NON-NLS-1$
}
b.append("</tr>"); //$NON-NLS-1$
// Making sure KnownList can be handled safely and produces the correct behaviour
if (spellSupport.hasKnownList() || spellSupport.hasKnownSpells(pc))
{
b.append("<tr><td><font size=-1><b>Known</b></font></td>"); //$NON-NLS-1$
for (int i = 0; i <= highestSpellLevel; ++i)
{
final int a = spellSupport.getKnownForLevel(i, pc);
final int bonus = spellSupport.getSpecialtyKnownForLevel(i, pc);
b.append("<td><font size=-1><center>"); //$NON-NLS-1$
b.append(String.valueOf(a));
if (bonus > 0)
{
b.append('+').append(Integer.toString(bonus));
}
b.append("</center></font></td>"); //$NON-NLS-1$
}
b.append("</tr>"); //$NON-NLS-1$
}
b.append("<tr><td><font size=-1><b>DC</b></font></td>"); //$NON-NLS-1$
for (int i = 0; i <= highestSpellLevel; ++i)
{
b.append("<td><font size=-1><center>"); //$NON-NLS-1$
b.append(String.valueOf(getDC(aClass, i, pc)));
b.append("</center></font></td>"); //$NON-NLS-1$
}
b.append("</tr></table>"); //$NON-NLS-1$
b.appendI18nElement("InfoSpells.caster.type", aClass.getSpellType()); //$NON-NLS-1$
b.appendLineBreak();
b.appendI18nElement("InfoSpells.stat.bonus", aClass.getSpellBaseStat()); //$NON-NLS-1$
if (pc.hasAssocs(aClass, AssociationKey.SPECIALTY) || charDisplay.hasDomains())
{
boolean needComma = false;
StringBuilder schoolInfo = new StringBuilder();
String spec = pc.getAssoc(aClass, AssociationKey.SPECIALTY);
if (spec != null)
{
schoolInfo.append(spec);
needComma = true;
}
for (Domain d : charDisplay.getSortedDomainSet())
{
if (needComma)
{
schoolInfo.append(',');
}
needComma = true;
schoolInfo.append(d.getKeyName());
}
b.appendLineBreak();
b.appendI18nElement("InfoSpells.school", schoolInfo.toString()); //$NON-NLS-1$
}
Set<String> set = new TreeSet<>();
for (SpellProhibitor sp : aClass
.getSafeListFor(ListKey.PROHIBITED_SPELLS))
{
set.addAll(sp.getValueList());
}
Collection<? extends SpellProhibitor> prohibList = charDisplay
.getProhibitedSchools(aClass);
if (prohibList != null)
{
for (SpellProhibitor sp : prohibList)
{
set.addAll(sp.getValueList());
}
}
if (!set.isEmpty())
{
b.appendLineBreak();
b.appendI18nElement("InfoSpells.prohibited.school", //$NON-NLS-1$
StringUtil.join(set, ",")); //$NON-NLS-1$
}
String bString = SourceFormat.getFormattedString(aClass,
Globals.getSourceDisplay(), true);
if (bString.length() > 0)
{
b.appendLineBreak();
b.appendI18nElement("in_source", bString); //$NON-NLS-1$
}
return b.toString();
}
private static String getNumCast(PCClass aClass, int level,
PlayerCharacter pc)
{
String sbook = Globals.getDefaultSpellBook();
final String cast =
pc.getSpellSupport(aClass).getCastForLevel(level, sbook, true, false, pc)
+ pc.getSpellSupport(aClass).getBonusCastForLevelString(level, sbook, pc);
return cast;
}
private static int getDC(PCClass aClass, int level, PlayerCharacter pc)
{
return pc.getDC(new Spell(), aClass, level, 0, aClass);
}
/**
* Construct the list of available spells for the character.
*/
private void buildAvailableNodes()
{
availableSpellNodes.clearContents();
// Scan character classes for spell classes
List<PCClass> classList = getCharactersSpellcastingClasses();
// Look at each spell on each spellcasting class
for (PCClass pcClass : classList)
{
DoubleKeyMapToList<SpellFacade, String, SpellNode> existingSpells =
buildExistingSpellMap(availableSpellNodes, pcClass);
for (Spell spell : pc.getAllSpellsInLists(charDisplay.getSpellLists(pcClass)))
{
// Create SpellNodeImpl for each spell
CharacterSpell charSpell = new CharacterSpell(pcClass, spell);
SpellFacadeImplem spellImplem =
new SpellFacadeImplem(pc, spell, charSpell, null);
HashMapToList<CDOMList<Spell>, Integer> levelInfo = pc.getSpellLevelInfo(spell);
for (CDOMList<Spell> spellList : charDisplay.getSpellLists(pcClass))
{
List<Integer> levels = levelInfo.getListFor(spellList);
if (levels != null)
{
for (Integer level : levels)
{
SpellNodeImpl node =
new SpellNodeImpl(spellImplem, pcClass,
String.valueOf(level), null);
if (!existingSpells.containsInList(spellImplem,
node.getSpellLevel(), node))
{
// Add to list
availableSpellNodes.addElement(node);
}
}
}
}
}
}
}
/**
* Create a map of the spell nodes for a class in the supplied list. This
* is intended to allow quick checking of the presence of a spell in the
* list.
*
* @param spellNodeList The list of spell nodes
* @param pcClass The class to filter the map by
* @return A double map to the class' spells from the list.
*/
private DoubleKeyMapToList<SpellFacade, String, SpellNode> buildExistingSpellMap(
DefaultListFacade<SpellNode> spellNodeList, PCClass pcClass)
{
DoubleKeyMapToList<SpellFacade, String, SpellNode> spellMap =
new DoubleKeyMapToList<>();
for (SpellNode spellNode : spellNodeList)
{
if (pcClass.equals(spellNode.getSpellcastingClass()))
{
spellMap.addToListFor(spellNode.getSpell(),
spellNode.getSpellLevel(), spellNode);
}
}
return spellMap;
}
/**
* Construct the list of spells the character knows, has prepared or has in
* a spell book.
*/
private void buildKnownPreparedNodes()
{
allKnownSpellNodes.clearContents();
knownSpellNodes.clearContents();
bookSpellNodes.clearContents();
preparedSpellNodes.clearContents();
// Ensure spell information is up to date
pc.getSpellList();
// Scan character classes for spell classes
List<PCClass> classList = getCharactersSpellcastingClasses();
List<PObject> pobjList = new ArrayList<>(classList);
// Include spells from race etc
pobjList.add(charDisplay.getRace());
// Look at each spell on each spellcasting class
for (PObject pcClass : pobjList)
{
buildKnownPreparedSpellsForCDOMObject(pcClass);
}
spellBooks.clear();
spellBookNames.clearContents();
for (SpellBook spellBook : charDisplay.getSpellBooks())
{
if (spellBook.getType() == SpellBook.TYPE_PREPARED_LIST)
{
DummySpellNodeImpl spellListNode =
new DummySpellNodeImpl(getRootNode(spellBook.getName()));
preparedSpellLists.add(spellListNode);
addDummyNodeIfSpellListEmpty(spellBook.getName());
}
else if (spellBook.getType() == SpellBook.TYPE_SPELL_BOOK)
{
DummySpellNodeImpl spellListNode =
new DummySpellNodeImpl(getRootNode(spellBook.getName()));
spellBooks.add(spellListNode);
addDummyNodeIfSpellBookEmpty(spellBook.getName());
spellBookNames.addElement(spellBook.getName());
}
}
}
private void buildKnownPreparedSpellsForCDOMObject(CDOMObject pObject)
{
Collection<? extends CharacterSpell> sp = charDisplay.getCharacterSpells(pObject);
List<CharacterSpell> cSpells = new ArrayList<>(sp);
// Add in the spells granted by objects
pc.addBonusKnownSpellsToList(pObject, cSpells);
PCClass pcClass = (PCClass) (pObject instanceof PCClass ? pObject : null);
for (CharacterSpell charSpell : cSpells)
{
for (SpellInfo spellInfo : charSpell.getInfoList())
{
// Create SpellNodeImpl for each spell
String book = spellInfo.getBook();
boolean isKnown = Globals.getDefaultSpellBook().equals(book);
SpellFacadeImplem spellImplem =
new SpellFacadeImplem(pc, charSpell.getSpell(),
charSpell, spellInfo);
SpellNodeImpl node;
if (pcClass != null)
{
node =
new SpellNodeImpl(spellImplem, pcClass,
String.valueOf(spellInfo.getActualLevel()),
getRootNode(book));
}
else
{
node =
new SpellNodeImpl(spellImplem,
String.valueOf(spellInfo.getActualLevel()),
getRootNode(book));
}
if (spellInfo.getTimes() > 1)
{
node.addCount(spellInfo.getTimes() -1);
}
boolean isSpellBook =
charDisplay.getSpellBookByName(book).getType() == SpellBook.TYPE_SPELL_BOOK;
// Add to list
if (isKnown)
{
allKnownSpellNodes.addElement(node);
knownSpellNodes.addElement(node);
}
else if (isSpellBook)
{
bookSpellNodes.addElement(node);
}
else if (pObject instanceof Race)
{
allKnownSpellNodes.addElement(node);
}
else
{
preparedSpellNodes.addElement(node);
}
}
}
}
/**
* Add a spell to the named book for the character. The request will be
* validated and any errors shown to the user by the UIDelegate.
*
* @param spell The spell to be added.
* @param bookName The book to add the spell to.
* @param metamagicFeats List of the metamagic feats that should be applied to this spell.
* @return The new SpellNode, or null if the selection was invalid.
*/
private SpellNode addSpellToCharacter(SpellNode spell, String bookName, List<Ability> metamagicFeats)
{
if (!(spell.getSpell() instanceof SpellFacadeImplem))
{
return null;
}
if (spell.getSpellcastingClass() == null)
{
return null;
}
CharacterSpell charSpell =
((SpellFacadeImplem) spell.getSpell()).getCharSpell();
if (charSpell == null)
{
return null;
}
int level = Integer.parseInt(spell.getSpellLevel());
for (Ability ability : metamagicFeats)
{
level += ability.getSafe(IntegerKey.ADD_SPELL_LEVEL);
}
String errorMsg =
pc.addSpell(charSpell, metamagicFeats, spell.getSpellcastingClass()
.getKeyName(), bookName, level, level);
if (!StringUtils.isEmpty(errorMsg))
{
delegate.showErrorMessage(Constants.APPLICATION_NAME, errorMsg);
return null;
}
SpellInfo spellInfo =
charSpell.getSpellInfoFor(bookName, level, metamagicFeats);
boolean isKnown = Globals.getDefaultSpellBook().equals(bookName);
SpellFacadeImplem spellImplem =
new SpellFacadeImplem(pc, charSpell.getSpell(), charSpell,
spellInfo);
SpellNodeImpl node =
new SpellNodeImpl(spellImplem, spell.getSpellcastingClass(),
String.valueOf(spellInfo.getActualLevel()), getRootNode(bookName));
return node;
}
private RootNodeImpl getRootNode(String bookName)
{
if (Globals.getDefaultSpellBook().equals(bookName))
{
return null;
}
RootNodeImpl rootNode = rootNodeMap.get(bookName);
if (rootNode == null)
{
SpellBook book = charDisplay.getSpellBookByName(bookName);
if (book == null)
{
return null;
}
rootNode = new RootNodeImpl(book);
rootNodeMap.put(bookName, rootNode);
}
return rootNode;
}
/**
* Remove a spell from the named book for the character. The request will be
* validated and any errors shown to the user by the UIDelegate.
*
* @param spell The spell to be removed.
* @param bookName The book to remove the spell from.
* @return True if the removal worked, false if the selection was invalid.
*/
private boolean removeSpellFromCharacter(SpellNode spell, String bookName)
{
if (!(spell.getSpell() instanceof SpellFacadeImplem))
{
return false;
}
SpellFacadeImplem sfi = (SpellFacadeImplem) spell.getSpell();
CharacterSpell charSpell = sfi.getCharSpell();
SpellInfo spellInfo = sfi.getSpellInfo();
if (charSpell == null || spellInfo == null)
{
return false;
}
final String errorMsg =
pc.delSpell(spellInfo, (PCClass) spell.getSpellcastingClass(),
bookName);
if (errorMsg.length() > 0)
{
delegate.showErrorMessage(Constants.APPLICATION_NAME, errorMsg);
ShowMessageDelegate.showMessageDialog(errorMsg,
Constants.APPLICATION_NAME, MessageType.ERROR);
return false;
}
return true;
}
private List<PCClass> getCharactersSpellcastingClasses()
{
List<PCClass> castingClasses = new ArrayList<>();
Collection<PCClass> classes = charDisplay.getClassSet();
for (PCClass pcClass : classes)
{
if (pcClass.get(FactKey.valueOf("SpellType")) != null)
{
SpellSupportForPCClass spellSupport = pc.getSpellSupport(pcClass);
if (spellSupport.canCastSpells(pc) || spellSupport.hasKnownList())
{
castingClasses.add(pcClass);
}
}
}
return castingClasses;
}
@Override
public boolean isAutoSpells()
{
return pc.getAutoSpells();
}
@Override
public void setAutoSpells(boolean autoSpells)
{
pc.setAutoSpells(autoSpells);
}
@Override
public boolean isUseHigherKnownSlots()
{
return pc.getUseHigherKnownSlots();
}
@Override
public void setUseHigherPreppedSlots(boolean useHigher)
{
pc.setUseHigherPreppedSlots(useHigher);
}
@Override
public boolean isUseHigherPreppedSlots()
{
return pc.getUseHigherPreppedSlots();
}
@Override
public void setUseHigherKnownSlots(boolean useHigher)
{
pc.setUseHigherKnownSlots(useHigher);
}
/**
* @return the list of spell books
*/
@Override
public ListFacade<String> getSpellbooks()
{
return spellBookNames;
}
/**
* @return the defaultSpellBook The name of the spell book to hold any new known spells.
*/
@Override
public DefaultReferenceFacade<String> getDefaultSpellBookRef()
{
return defaultSpellBook;
}
/**
* Set the spell book to hold any new known spells.
* @param bookName The name of the new default spell book.
*/
@Override
public void setDefaultSpellBook(String bookName)
{
SpellBook book = charDisplay.getSpellBookByName(bookName);
if (book == null || book.getType() != SpellBook.TYPE_SPELL_BOOK)
{
return;
}
pc.setSpellBookNameToAutoAddKnown(bookName);
defaultSpellBook.set(bookName);
}
@Override
public void elementAdded(ListEvent<EquipmentFacade> e)
{
updateSpellBooks((Equipment) e.getElement());
}
@Override
public void elementRemoved(ListEvent<EquipmentFacade> e)
{
updateSpellBooks((Equipment) e.getElement());
}
@Override
public void elementsChanged(ListEvent<EquipmentFacade> e)
{
updateSpellBooks((Equipment) e.getElement());
}
@Override
public void elementModified(ListEvent<EquipmentFacade> e)
{
updateSpellBooks((Equipment) e.getElement());
}
@Override
public void quantityChanged(EquipmentListEvent e)
{
updateSpellBooks((Equipment) e.getEquipment());
}
/**
* Update the stored spellbook details in response to a spellbook item of
* equipment changing. Changes to equipment other than spellbooks will be
* ignored.
* @param equip The equipment item that changed.
*/
private void updateSpellBooks(Equipment equip)
{
if (equip == null || equip.isType("SPELLBOOK"))
{
buildKnownPreparedNodes();
for (Iterator<SpellNode> iterator = bookSpellNodes.iterator(); iterator.hasNext();)
{
SpellNode spell = iterator.next();
if (!spellBookNames.containsElement(spell.getRootNode().getName()))
{
iterator.remove();
}
}
}
}
public class RootNodeImpl implements RootNode
{
private final String name;
private final SpellBook book;
public RootNodeImpl(String text)
{
this.name = text;
this.book = null;
}
public RootNodeImpl(SpellBook book)
{
this.book = book;
this.name = book.getName();
}
@Override
public String toString()
{
if (book != null)
{
return book.toString();
}
return name;
}
@Override
public String getName()
{
return name;
}
@Override
public int hashCode()
{
int hash = 7;
hash = 83 * hash + (this.name != null ? this.name.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final RootNodeImpl other = (RootNodeImpl) obj;
if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name))
{
return false;
}
return true;
}
}
@Override
public void previewSpells()
{
boolean aBool = SettingsHandler.getPrintSpellsWithPC();
SettingsHandler.setPrintSpellsWithPC(true);
String templateFileName = PCGenSettings.getInstance().getProperty(
PCGenSettings.SELECTED_SPELL_SHEET_PATH);
if (StringUtils.isEmpty(templateFileName))
{
delegate.showErrorMessage(Constants.APPLICATION_NAME,
LanguageBundle.getString("in_spellNoSheet")); //$NON-NLS-1$
return;
}
File templateFile = new File(templateFileName);
File outputFile = BatchExporter.getTempOutputFilename(templateFile);
boolean success;
if (ExportUtilities.isPdfTemplate(templateFile))
{
success = BatchExporter.exportCharacterToPDF(pcFacade, outputFile, templateFile);
}
else
{
success = BatchExporter.exportCharacterToNonPDF(pcFacade, outputFile, templateFile);
}
if (success)
{
try
{
Utility.viewInBrowser(outputFile);
}
catch (IOException e)
{
Logging.errorPrint("SpellSupportFacadeImpl.previewSpells failed", e);
delegate.showErrorMessage(Constants.APPLICATION_NAME,
LanguageBundle.getString("in_spellPreviewFail")); //$NON-NLS-1$
}
}
SettingsHandler.setPrintSpellsWithPC(aBool);
}
@Override
public void exportSpells()
{
final String template = PCGenSettings.getInstance().getProperty(
PCGenSettings.SELECTED_SPELL_SHEET_PATH);
if (StringUtils.isEmpty(template))
{
delegate.showErrorMessage(Constants.APPLICATION_NAME,
LanguageBundle.getString("in_spellNoSheet")); //$NON-NLS-1$
return;
}
String ext = template.substring(template.lastIndexOf('.'));
// Get the name of the file to output to.
JFileChooser fcExport = new JFileChooser();
fcExport.setCurrentDirectory(new File(PCGenSettings.getPcgDir()));
fcExport.setDialogTitle(LanguageBundle
.getString("InfoSpells.export.spells.for") + charDisplay.getDisplayName()); //$NON-NLS-1$
if (fcExport.showSaveDialog(null) != JFileChooser.APPROVE_OPTION)
{
return;
}
final String aFileName = fcExport.getSelectedFile().getAbsolutePath();
if (aFileName.length() < 1)
{
delegate.showErrorMessage(Constants.APPLICATION_NAME,
LanguageBundle.getString("InfoSpells.must.set.filename")); //$NON-NLS-1$
return;
}
try
{
final File outFile = new File(aFileName);
if (outFile.isDirectory())
{
delegate.showErrorMessage(Constants.APPLICATION_NAME,
LanguageBundle.getString("InfoSpells.can.not.overwrite.directory")); //$NON-NLS-1$
return;
}
if (outFile.exists())
{
int reallyClose =
JOptionPane
.showConfirmDialog(
null,
LanguageBundle
.getFormattedString(
"InfoSpells.confirm.overwrite", outFile.getName()), //$NON-NLS-1$
LanguageBundle
.getFormattedString(
"InfoSpells.overwriting", outFile.getName()), JOptionPane.YES_NO_OPTION); //$NON-NLS-1$
if (reallyClose != JOptionPane.YES_OPTION)
{
return;
}
}
// Output the file
File templateFile = new File(template);
boolean success;
if (ExportUtilities.isPdfTemplate(templateFile))
{
success = BatchExporter.exportCharacterToPDF(pcFacade, outFile, templateFile);
}
else
{
success = BatchExporter.exportCharacterToNonPDF(pcFacade, outFile, templateFile);
}
if (!success)
{
delegate.showErrorMessage(Constants.APPLICATION_NAME,
LanguageBundle.getFormattedString(
"InfoSpells.export.failed", charDisplay.getDisplayName())); //$NON-NLS-1$
}
}
catch (Exception ex)
{
Logging.errorPrint(LanguageBundle.getFormattedString(
"InfoSpells.export.failed", charDisplay.getDisplayName()), ex); //$NON-NLS-1$
delegate.showErrorMessage(Constants.APPLICATION_NAME,
LanguageBundle.getFormattedString(
"InfoSpells.export.failed.retry", charDisplay.getDisplayName())); //$NON-NLS-1$
}
}
/**
* The Class <code>SpellNodeImpl</code> holds the information required to
* display and process a spell. It covers spells that are available, known,
* memorised etc.
*
* @author James Dempsey <jdempsey@users.sourceforge.net>
*/
public class SpellNodeImpl implements SpellNode
{
private final SpellFacade spell;
private final ClassFacade cls;
private final RootNode rootNode;
private final String level;
private int count;
/**
* Create a new instance of SpellNodeImpl for a class spell list.
* @param spell The spell the node represents.
* @param cls The character class for the spell.
* @param level The level of the spell.
* @param rootNode The top level node this entry will appear under.
*/
public SpellNodeImpl(SpellFacade spell, ClassFacade cls, String level, RootNode rootNode)
{
this.spell = spell;
this.cls = cls;
this.level = level;
this.rootNode = rootNode;
this.count = 1;
}
/**
* Create a new instance of SpellNodeImpl for a non class based spell list.
* @param spell The spell the node represents.
* @param level The level of the spell.
* @param rootNode The top level node this entry will appear under.
*/
public SpellNodeImpl(SpellFacade spell, String level, RootNode rootNode)
{
this(spell, (ClassFacade) null, level, rootNode);
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade.SpellNode#getSpellcastingClass()
*/
@Override
public ClassFacade getSpellcastingClass()
{
return cls;
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade.SpellNode#getSpellLevel()
*/
@Override
public String getSpellLevel()
{
return level;
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade.SpellNode#getSpell()
*/
@Override
public SpellFacade getSpell()
{
return spell;
}
/* (non-Javadoc)
* @see pcgen.core.facade.SpellSupportFacade.SpellNode#getRootNode()
*/
@Override
public RootNode getRootNode()
{
return rootNode;
}
@Override
public int getCount()
{
return count;
}
@Override
public void addCount(int num)
{
count += num;
}
@Override
public String toString()
{
String countStr = "";
if (count != 1)
{
countStr = " (x" + count + ")"; //$NON-NLS-1$ //$NON-NLS-2$
}
if (spell != null)
{
return spell.toString() + countStr;
}
else if (cls != null)
{
return cls.toString() + countStr;
}
else if (rootNode != null)
{
return rootNode.toString() + countStr;
}
return LanguageBundle.getFormattedString("in_spellEmptyNode", countStr); //$NON-NLS-1$
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + getOuterType().hashCode();
result = prime * result + ((cls == null) ? 0 : cls.hashCode());
result = prime * result + ((level == null) ? 0 : level.hashCode());
result =
prime * result
+ ((rootNode == null) ? 0 : rootNode.hashCode());
result = prime * result + ((spell == null) ? 0 : spell.hashCode());
return result;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
SpellNodeImpl other = (SpellNodeImpl) obj;
if (!getOuterType().equals(other.getOuterType()))
{
return false;
}
if (level == null)
{
if (other.level != null)
{
return false;
}
}
else if (!level.equals(other.level))
{
return false;
}
if (spell == null)
{
if (other.spell != null)
{
return false;
}
}
else if (!spell.equals(other.spell))
{
return false;
}
if (cls == null)
{
if (other.cls != null)
{
return false;
}
}
else if (!cls.equals(other.cls))
{
return false;
}
if (rootNode == null)
{
if (other.rootNode != null)
{
return false;
}
}
else if (!rootNode.equals(other.rootNode))
{
return false;
}
return true;
}
private SpellSupportFacadeImpl getOuterType()
{
return SpellSupportFacadeImpl.this;
}
}
/**
* The Class <code>DummySpellNodeImpl</code> holds the information required to
* display an empty spell list. It is only used to ensure the spell list name
* is displayed.
*
* @author James Dempsey <jdempsey@users.sourceforge.net>
*/
public class DummySpellNodeImpl implements SpellNode
{
private final RootNode rootNode;
/**
* Create a new instance of DummySpellNodeImpl
* @param rootNode The root node for the spell list.
*/
public DummySpellNodeImpl(RootNode rootNode)
{
this.rootNode = rootNode;
}
@Override
public ClassFacade getSpellcastingClass()
{
return null;
}
@Override
public String getSpellLevel()
{
return "";
}
@Override
public SpellFacade getSpell()
{
return null;
}
@Override
public RootNode getRootNode()
{
return rootNode;
}
@Override
public int getCount()
{
return 1;
}
@Override
public void addCount(int num)
{
// Ignored.
}
@Override
public String toString()
{
return LanguageBundle.getString("in_spellEmptySpellList"); //$NON-NLS-1$
}
}
}