/*
* SpellBuilderFacadeImpl.java
* Copyright 2013 (C) James Dempsey <jdempsey@users.sourceforge.net>
*
* 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 10/10/2013
*
* $Id$
*/
package pcgen.gui2.facade;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import org.apache.commons.lang3.StringUtils;
import pcgen.base.util.HashMapToList;
import pcgen.cdom.base.CDOMList;
import pcgen.cdom.base.Constants;
import pcgen.cdom.enumeration.DataSetID;
import pcgen.cdom.enumeration.IntegerKey;
import pcgen.cdom.enumeration.ListKey;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.cdom.enumeration.Type;
import pcgen.cdom.facet.FacetLibrary;
import pcgen.cdom.facet.MasterAvailableSpellFacet;
import pcgen.cdom.helper.AvailableSpell;
import pcgen.cdom.identifier.SpellSchool;
import pcgen.cdom.list.ClassSpellList;
import pcgen.cdom.list.DomainSpellList;
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.SettingsHandler;
import pcgen.facade.core.AbilityFacade;
import pcgen.facade.util.DefaultReferenceFacade;
import pcgen.facade.core.InfoFacade;
import pcgen.facade.util.ReferenceFacade;
import pcgen.facade.core.SpellBuilderFacade;
import pcgen.facade.util.DefaultListFacade;
import pcgen.facade.util.ListFacade;
import pcgen.core.spell.Spell;
import pcgen.util.Logging;
/**
* SpellBuilderFacadeImpl prepares the data for display in the Spell Choice
* Dialog. It also manages the user's selections and updates the available
* choices to match the choices already made.
*
*
* @author James Dempsey <jdempsey@users.sourceforge.net>
*/
public class SpellBuilderFacadeImpl implements SpellBuilderFacade
{
private DefaultReferenceFacade<InfoFacade> pcClass;
private DefaultReferenceFacade<Integer> spellLevel;
private DefaultReferenceFacade<InfoFacade> spell;
private DefaultReferenceFacade<String> variant;
private DefaultReferenceFacade<Integer> casterLevel;
private DefaultReferenceFacade<String> spellType;
private DefaultListFacade<InfoFacade> availClasses;
private DefaultListFacade<Integer> availSpellLevels;
private DefaultListFacade<InfoFacade> availSpells;
private DefaultListFacade<String> availVariants;
private DefaultListFacade<Integer> availCasterlevels;
private DefaultListFacade<String> availSpellTypes;
private DefaultListFacade<AbilityFacade> availMetamagicFeats;
private DefaultListFacade<AbilityFacade> selMetamagicFeats;
private List<String> classList;
private List<String> levelList;
private boolean metaAllowed;
private Boolean spellBooks;
private int minSpellLevel = 0;
private int maxSpellLevel = 9;
private String reqSpellType = "";
private List<String> subTypeList = new ArrayList<>();
private PlayerCharacter character;
private Type requiredType;
private List<AvailableSpell> classSpells;
private CDOMList<Spell> spellList;
private MasterAvailableSpellFacet masterAvailableSpellFacet;
private DataSetID datasetID;
/**
* Create a new instance SpellBuilderFacadeImpl to manage a particular
* spell choice.
*
* @param choiceValue The string defining the choice. Should not include the EQBUILDER.SPELL| tag itself.
* @param character The character which the item will belong to.
* @param equip The equipment, if any, that the spell will be associated with.
*/
public SpellBuilderFacadeImpl(String choiceValue,
PlayerCharacter character, Equipment equip)
{
this.character = character;
masterAvailableSpellFacet = FacetLibrary.getFacet(MasterAvailableSpellFacet.class);
datasetID = character.getCharID().getDatasetID();
availClasses = new DefaultListFacade<>();
availSpellLevels = new DefaultListFacade<>();
availSpells = new DefaultListFacade<>();
availVariants = new DefaultListFacade<>();
availCasterlevels = new DefaultListFacade<>();
availSpellTypes = new DefaultListFacade<>();
availMetamagicFeats = new DefaultListFacade<>();
pcClass = new DefaultReferenceFacade<>();
spellLevel = new DefaultReferenceFacade<>();
spell = new DefaultReferenceFacade<>();
variant = new DefaultReferenceFacade<>();
casterLevel = new DefaultReferenceFacade<>();
spellType = new DefaultReferenceFacade<>();
selMetamagicFeats = new DefaultListFacade<>();
requiredType = Type.NONE;
if (equip != null)
{
Type[] knownTypes =
new Type[]{Type.POTION, Type.SCROLL, Type.WAND, Type.RING};
for (Type itemType : knownTypes)
{
if (equip.isType(itemType.toString()))
{
requiredType = itemType;
break;
}
}
}
parseChoiceValue(choiceValue);
buildLists();
if (availClasses.getSize() > 0)
{
setClass(availClasses.getElementAt(0));
}
}
/**
* Parse the choice string, if one was supplied.
* @param choiceValue The value to be parsed.
*/
private void parseChoiceValue(String choiceValue)
{
classList = null;
levelList = null;
metaAllowed = true;
spellBooks = null;
if (StringUtils.isNotEmpty(choiceValue))
{
if (!parseClassLevelSyntax(choiceValue))
{
parseTypeSyntax(choiceValue);
}
}
// Add in any relevant restrictions from preferences on crafting
if (requiredType == Type.POTION)
{
maxSpellLevel =
Math.min(maxSpellLevel,
SettingsHandler.getMaxPotionSpellLevel());
}
else if (requiredType == Type.WAND)
{
maxSpellLevel =
Math.min(maxSpellLevel,
SettingsHandler.getMaxWandSpellLevel());
}
}
/**
* Parse the 'hidden' syntax (CMP?) allowing class and level selection.
* Format: CLASS=Wizard|CLASS=Sorcerer|Metamagic=0|LEVEL=1|LEVEL=2|SPELLBOOKS=Y
* @param choiceValue The value to be parsed.
* @return true if this is a class level syntax value.
*/
private boolean parseClassLevelSyntax(String choiceValue)
{
//
// CLASS=Wizard|CLASS=Sorcerer|Metamagic=0|LEVEL=1|LEVEL=2|SPELLBOOKS=Y
final StringTokenizer aTok =
new StringTokenizer(choiceValue, "|", false);
while (aTok.hasMoreTokens())
{
String aString = aTok.nextToken();
if (aString.startsWith("CLASS="))
{
if (classList == null)
{
classList = new ArrayList<>();
}
classList.add(aString.substring(6));
}
else if (aString.startsWith("LEVEL="))
{
if (levelList == null)
{
levelList = new ArrayList<>();
}
levelList.add(aString.substring(6));
}
else if (aString.startsWith("SPELLBOOKS="))
{
switch (aString.charAt(11))
{
case 'Y':
spellBooks = true;
break;
case 'N':
spellBooks = false;
break;
default:
spellBooks = null;
break;
}
}
else if (aString.equals("METAMAGIC=N"))
{
metaAllowed = false;
}
else
{
return false;
}
}
return true;
}
/**
* Parse the standard syntax allowing selection on types and spell levels.
* @param choiceValue The value to be parsed.
*/
private void parseTypeSyntax(String choiceValue)
{
StringTokenizer aTok = new StringTokenizer(choiceValue, "|");
if (aTok.hasMoreTokens())
{
reqSpellType = aTok.nextToken();
if (reqSpellType.equalsIgnoreCase("ANY")
|| reqSpellType.equalsIgnoreCase("ALL"))
{
reqSpellType = "";
}
}
while (aTok.hasMoreTokens())
{
String aString = aTok.nextToken();
try
{
minSpellLevel = Integer.parseInt(aString);
break;
}
catch (NumberFormatException nfe)
{
subTypeList.add(aString);
}
}
if (aTok.hasMoreTokens())
{
maxSpellLevel = Integer.parseInt(aTok.nextToken());
}
}
/**
* Use the parsed rules to build up the lists that will not change,
* such as class/domain list and metamagic feats.
*/
private void buildLists()
{
List<PCClass> classes = new ArrayList<>();
List<Domain> domains = new ArrayList<>();
if (classList != null)
{
for (String classKey : classList)
{
PObject obj =
Globals.getContext().getReferenceContext()
.silentlyGetConstructedCDOMObject(PCClass.class,
classKey);
if (obj == null)
{
obj =
Globals.getContext().getReferenceContext()
.silentlyGetConstructedCDOMObject(Domain.class,
classKey);
if (obj != null)
{
domains.add((Domain) obj);
}
}
else
{
classes.add((PCClass) obj);
}
}
}
else
{
for (Spell spell : Globals.getContext().getReferenceContext()
.getConstructedCDOMObjects(Spell.class))
{
if (isSpellOfSubType(spell))
{
addSpellInfoToList(spell, classes, domains, reqSpellType);
}
}
for (PCClass aClass : Globals.getContext().getReferenceContext()
.getConstructedCDOMObjects(PCClass.class))
{
if (!aClass.getSpellType().equals(Constants.NONE))
{
// Only adds if the class can cast
if (character.getSpellSupport(aClass).canCastSpells(
character))
{
continue;
}
if (!("".equals(reqSpellType))
&& (reqSpellType.indexOf(aClass.getSpellType()) < 0))
{
continue;
}
if (!classes.contains(aClass))
{
classes.add(aClass);
}
}
}
}
if (spellBooks != null)
{
for (int i = classes.size() - 1; i >= 0; --i)
{
PCClass obj = classes.get(i);
if (!spellBooks) // can't have books
{
if (obj.getSafe(ObjectKey.SPELLBOOK))
{
classes.remove(i);
}
}
else
// must have books
{
if (!(obj instanceof PCClass)
|| !obj.getSafe(ObjectKey.SPELLBOOK))
{
classes.remove(i);
}
}
}
if (spellBooks)
{
domains.clear();
}
}
List<InfoFacade> allObjects = new ArrayList<>();
Globals.sortPObjectListByName(classes);
allObjects.addAll(classes);
Globals.sortPObjectListByName(domains);
allObjects.addAll(domains);
availClasses.setContents(allObjects);
// Spell levels
List<Integer> spellLevelValues = new ArrayList<>();
if ((levelList != null) && (!levelList.isEmpty()))
{
for (int i = minSpellLevel; i < levelList.size(); ++i)
{
spellLevelValues.add(Integer.valueOf(levelList.get(i)));
}
}
else
{
for (int i = minSpellLevel; i <= maxSpellLevel; i++)
{
spellLevelValues.add(i);
}
}
availSpellLevels.setContents(spellLevelValues);
// Caster levels
updateAvailCasterLevels(1, 20);
//Metamagic
if (metaAllowed)
{
List<Ability> metamagicFeats = new ArrayList<>();
for (Ability anAbility : Globals.getContext().getReferenceContext().getManufacturer(
Ability.class, AbilityCategory.FEAT).getAllObjects())
{
if (anAbility.isType("Metamagic"))
{
metamagicFeats.add(anAbility);
}
}
Globals.sortPObjectListByName(metamagicFeats);
availMetamagicFeats.setContents(metamagicFeats);
}
}
private void addSpellInfoToList(final Spell aSpell, List<PCClass> classes,
List<Domain> domains, String spellType)
{
Set<String> unfoundItems = new HashSet<>();
final HashMapToList<CDOMList<Spell>, Integer> levelInfo =
character.getSpellLevelInfo(aSpell);
if ((levelInfo == null) || (levelInfo.isEmpty()))
{
// Logging.errorPrint("Spell: "
// + aSpell.getKeyName()
// + "("
// + SourceFormat.getFormattedString(aSpell,
// Globals.getSourceDisplay(), true) + ") has no home");
return;
}
for (CDOMList<Spell> spellList : levelInfo.getKeySet())
{
if (spellList instanceof ClassSpellList)
{
String key = spellList.getKeyName();
final PCClass aClass =
Globals.getContext().getReferenceContext()
.silentlyGetConstructedCDOMObject(PCClass.class,
key);
if (aClass != null)
{
if (!("".equals(spellType))
&& (spellType.indexOf(aClass.getSpellType()) < 0))
{
continue;
}
if (!classes.contains(aClass))
{
classes.add(aClass);
}
}
else
{
key = 'C' + key;
if (!unfoundItems.contains(key))
{
unfoundItems.add(key);
Logging.errorPrint("Class " + key.substring(1)
+ " not found. Was used in spell " + aSpell);
}
}
}
else if (spellList instanceof DomainSpellList)
{
if (!("".equals(spellType))
&& (spellType.indexOf("Divine") < 0))
{
continue;
}
String key = spellList.getKeyName();
final Domain aDomain =
Globals.getContext().getReferenceContext()
.silentlyGetConstructedCDOMObject(Domain.class, key);
if (aDomain != null)
{
if (!domains.contains(aDomain))
{
domains.add(aDomain);
}
}
else
{
key = 'D' + key;
if (!unfoundItems.contains(key))
{
unfoundItems.add(key);
Logging.errorPrint("Domain " + key.substring(1)
+ " not found. Was used in spell " + aSpell);
}
}
}
else
{
Logging.errorPrint("Unknown spell source: " + spellList);
}
}
}
private boolean isSpellOfSubType(Spell aSpell)
{
if (subTypeList.isEmpty())
{
return true;
}
boolean finalIsOfType = false;
for (String s : subTypeList)
{
boolean isOfType = true;
StringTokenizer aTok = new StringTokenizer(s, ";,");
while (aTok.hasMoreTokens())
{
String subType = aTok.nextToken();
if (subType.startsWith("SCHOOL."))
{
SpellSchool ss =
Globals.getContext().getReferenceContext()
.silentlyGetConstructedCDOMObject(
SpellSchool.class, subType.substring(7));
if (ss == null
|| !aSpell.containsInList(ListKey.SPELL_SCHOOL, ss))
{
isOfType = false;
break;
}
}
if (subType.startsWith("SUBSCHOOL."))
{
if (!aSpell.containsInList(ListKey.SPELL_SUBSCHOOL,
subType.substring(10)))
{
isOfType = false;
break;
}
}
if (subType.startsWith("DESCRIPTOR."))
{
String descriptor = subType.substring(11);
if (!aSpell.containsInList(ListKey.SPELL_DESCRIPTOR,
descriptor))
{
isOfType = false;
break;
}
}
}
if (isOfType)
{
finalIsOfType = true;
break;
}
}
return finalIsOfType;
}
private void updateAvailCasterLevels(int min, int max)
{
List<Integer> levelsForCasting = new ArrayList<>(20);
for (int i = min; i <= max; i++)
{
levelsForCasting.add(i);
}
availCasterlevels.setContents(levelsForCasting);
}
/**
* Update lists that depend on the selected level of spell
* e.g. the list of spells
*/
private void processLevelChange()
{
int baseSpellLevel = spellLevel.get();
// List of available spells
List<Spell> spellsOfLevel = new ArrayList<>();
for (AvailableSpell availSpell : classSpells)
{
if (availSpell.getLevel() == baseSpellLevel)
{
spellsOfLevel.add(availSpell.getSpell());
}
}
Globals.sortPObjectListByName(spellsOfLevel);
availSpells.setContents(spellsOfLevel);
InfoFacade selSpell = spell.get();
if (selSpell == null || !spellsOfLevel.contains(selSpell))
{
Spell newSpell = null;
if (!spellsOfLevel.isEmpty())
{
newSpell = spellsOfLevel.get(0);
}
selectSpell(newSpell);
}
// Spell type
List<String> spellTypeList = getSpellTypeList();
availSpellTypes.setContents(spellTypeList);
spellType.set(spellTypeList.get(0));
}
private void selectSpell(Spell newSpell)
{
spell.set(newSpell);
// Handle variants
List<String> variants = new ArrayList<>();
;
if (newSpell != null)
{
variants = newSpell.getSafeListFor(ListKey.VARIANTS);
}
Collections.sort(variants);
availVariants.setContents(variants);
if (variants.isEmpty())
{
variant.set(null);
}
else
{
String currVariant = variant.get();
if (currVariant != null && !variants.contains(currVariant))
{
variant.set(null);
}
}
recalcCasterLevelDetails();
}
private void recalcCasterLevelDetails()
{
// Metamagic
int levelAdjust = 0;
for (AbilityFacade feat : selMetamagicFeats)
{
levelAdjust += ((Ability) feat).getSafe(IntegerKey.ADD_SPELL_LEVEL);
}
// Limit Caster level
int minClassLevel = 1;
int maxClassLevel = 20;
PCClass aClass;
InfoFacade castingClass = pcClass.get();
if (castingClass instanceof PCClass)
{
aClass = (PCClass) castingClass;
}
else if (castingClass instanceof Domain)
{
// TODO We should not be hardcoding the link between cleric and domains
aClass =
Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(
PCClass.class, "Cleric");
}
else
{
Logging
.errorPrint("Found Casting Class in recalc that was not a Class or Domain: "
+ castingClass.getClass());
return;
}
if (aClass != null)
{
minClassLevel =
character.getSpellSupport(aClass).getMinLevelForSpellLevel(
spellLevel.get() + levelAdjust, true);
minClassLevel = Math.max(1, minClassLevel);
if (aClass.hasMaxLevel())
{
maxClassLevel = aClass.getSafe(IntegerKey.LEVEL_LIMIT);
}
}
updateAvailCasterLevels(minClassLevel, maxClassLevel);
int currCasterLevel =
casterLevel.get() == null ? 0 : casterLevel
.get();
if (currCasterLevel < minClassLevel)
{
casterLevel.set(minClassLevel);
}
else if (currCasterLevel > maxClassLevel)
{
casterLevel.set(maxClassLevel);
}
}
private List<String> getSpellTypeList()
{
List<String> spellTypes = new ArrayList<>();
InfoFacade castingClass = pcClass.get();
if (castingClass instanceof PCClass)
{
spellTypes.add(((PCClass) castingClass).getSpellType());
}
else if (castingClass instanceof Domain)
{
spellTypes.add("Divine");
}
else
{
Logging
.errorPrint("Found Casting Class that was not a Class or Domain: "
+ castingClass.getClass());
}
return spellTypes;
}
private boolean canCreateItem(Spell aSpell)
{
if (requiredType == Type.NONE)
{
return true;
}
return aSpell.isAllowed(requiredType);
}
@Override
public void setClass(InfoFacade classFacade)
{
pcClass.set(classFacade);
if (classFacade instanceof Domain)
{
spellList = ((Domain) classFacade).get(ObjectKey.DOMAIN_SPELLLIST);
}
else
{
spellList = ((PCClass) classFacade).get(ObjectKey.CLASS_SPELLLIST);
}
classSpells = new ArrayList<>();
for (AvailableSpell availSpell : masterAvailableSpellFacet.getAllSpellsInList(spellList, datasetID))
{
if (canCreateItem(availSpell.getSpell()))
{
classSpells.add(availSpell);
}
}
if (spellLevel.get() == null)
{
spellLevel.set(availSpellLevels.getElementAt(0));
}
processLevelChange();
}
@Override
public ReferenceFacade<InfoFacade> getClassRef()
{
return pcClass;
}
@Override
public ListFacade<InfoFacade> getClasses()
{
return availClasses;
}
@Override
public void setSpellLevel(Integer newSpellLevel)
{
spellLevel.set(newSpellLevel);
processLevelChange();
}
@Override
public ListFacade<Integer> getLevels()
{
return availSpellLevels;
}
@Override
public ReferenceFacade<Integer> getSpellLevelRef()
{
return spellLevel;
}
@Override
public void setSpell(InfoFacade spellFacade)
{
if (spellFacade instanceof Spell)
{
selectSpell((Spell) spellFacade);
}
}
@Override
public ReferenceFacade<InfoFacade> getSpellRef()
{
return spell;
}
@Override
public ListFacade<InfoFacade> getSpells()
{
return availSpells;
}
@Override
public void setVariant(String newVariant)
{
variant.set(newVariant);
}
@Override
public ReferenceFacade<String> getVariantRef()
{
return variant;
}
@Override
public ListFacade<String> getVariants()
{
return availVariants;
}
@Override
public void setCasterLevel(Integer newCasterLevel)
{
casterLevel.set(newCasterLevel);
}
@Override
public ReferenceFacade<Integer> getCasterLevelRef()
{
return casterLevel;
}
@Override
public ListFacade<Integer> getCasterLevels()
{
return availCasterlevels;
}
@Override
public void setSpellType(String newSpellType)
{
spellType.set(newSpellType);
}
@Override
public ReferenceFacade<String> getSpellTypeRef()
{
return spellType;
}
@Override
public ListFacade<String> getSpellTypes()
{
return availSpellTypes;
}
@Override
public void setSelectedMetamagicFeats(Object[] newFeats)
{
List<AbilityFacade> chosenFeats = new ArrayList<>();
for (Object choice : newFeats)
{
if (choice instanceof AbilityFacade)
{
chosenFeats.add((AbilityFacade) choice);
}
}
selMetamagicFeats.setContents(chosenFeats);
recalcCasterLevelDetails();
}
@Override
public ListFacade<AbilityFacade> getSelectedMetamagicFeats()
{
return selMetamagicFeats;
}
@Override
public ListFacade<AbilityFacade> getAvailMetamagicFeats()
{
return availMetamagicFeats;
}
}