/*
* 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;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import pcgen.base.lang.StringUtil;
import pcgen.base.util.CaseInsensitiveMap;
import pcgen.base.util.TreeMapToList;
import pcgen.cdom.base.CDOMObject;
import pcgen.cdom.content.DamageReduction;
import pcgen.cdom.enumeration.CharID;
import pcgen.cdom.enumeration.ListKey;
import pcgen.cdom.facet.base.AbstractSourcedListFacet;
import pcgen.cdom.facet.event.DataFacetChangeEvent;
import pcgen.cdom.facet.event.DataFacetChangeListener;
/**
* DamageReductionFacet is a Facet that tracks the DamageReduction objects that
* have been granted to a Player Character.
*
* @author Thomas Parker (thpr [at] yahoo.com)
*/
public class DamageReductionFacet extends
AbstractSourcedListFacet<CharID, DamageReduction> implements
DataFacetChangeListener<CharID, CDOMObject>
{
private static final Pattern OR_PATTERN = Pattern.compile(" [oO][rR] ");
private static final Pattern AND_PATTERN = Pattern.compile(" [aA][nN][dD] ");
private PrerequisiteFacet prerequisiteFacet;
private FormulaResolvingFacet formulaResolvingFacet;
private BonusCheckingFacet bonusCheckingFacet;
private CDOMObjectConsolidationFacet consolidationFacet;
/**
* Extracts DamageReduction objects from CDOMObjects granted to a Player
* Character. The DamageReduction objects are granted to the Player
* Character.
*
* Triggered when one of the Facets to which DamageReductionFacet listens
* fires a DataFacetChangeEvent to indicate a CDOMObject was added to a
* Player Character.
*
* @param dfce
* The DataFacetChangeEvent containing the information about the
* change
*
* @see pcgen.cdom.facet.event.DataFacetChangeListener#dataAdded(pcgen.cdom.facet.event.DataFacetChangeEvent)
*/
@Override
public void dataAdded(DataFacetChangeEvent<CharID, CDOMObject> dfce)
{
CDOMObject cdo = dfce.getCDOMObject();
List<DamageReduction> drs = cdo.getListFor(ListKey.DAMAGE_REDUCTION);
if (drs != null)
{
addAll(dfce.getCharID(), drs, cdo);
}
}
/**
* Extracts DamageReduction objects from CDOMObjects removed from a Player
* Character. The DamageReduction objects are removed from to the Player
* Character.
*
* Triggered when one of the Facets to which DamageReductionFacet listens
* fires a DataFacetChangeEvent to indicate a CDOMObject was removed from a
* Player Character.
*
* @param dfce
* The DataFacetChangeEvent containing the information about the
* change
*
* @see pcgen.cdom.facet.event.DataFacetChangeListener#dataRemoved(pcgen.cdom.facet.event.DataFacetChangeEvent)
*/
@Override
public void dataRemoved(DataFacetChangeEvent<CharID, CDOMObject> dfce)
{
removeAll(dfce.getCharID(), dfce.getCDOMObject());
}
private CaseInsensitiveMap<Integer> getDRMap(CharID id,
Map<DamageReduction, Set<Object>> componentMap)
{
CaseInsensitiveMap<Integer> andMap = new CaseInsensitiveMap<>();
if (componentMap == null || componentMap.isEmpty())
{
return andMap;
}
CaseInsensitiveMap<Integer> orMap = new CaseInsensitiveMap<>();
for (Map.Entry<DamageReduction, Set<Object>> me : componentMap
.entrySet())
{
DamageReduction dr = me.getKey();
for (Object source : me.getValue())
{
if (prerequisiteFacet.qualifies(id, dr, source))
{
String sourceString = (source instanceof CDOMObject) ? ((CDOMObject) source)
.getQualifiedKey()
: "";
int rawDrValue = formulaResolvingFacet.resolve(id,
dr.getReduction(), sourceString).intValue();
String bypass = dr.getBypass();
if (OR_PATTERN.matcher(bypass).find())
{
Integer current = orMap.get(bypass);
if ((current == null)
|| (current.intValue() < rawDrValue))
{
orMap.put(dr.getBypass(), rawDrValue);
}
}
else
{
/*
* TODO Shouldn't this expansion be done in the DR
* token? (since it's static?)
*/
String[] splits = AND_PATTERN.split(bypass);
if (splits.length == 1)
{
Integer current = andMap.get(dr.getBypass());
if ((current == null)
|| (current.intValue() < rawDrValue))
{
andMap.put(dr.getBypass(), rawDrValue);
}
}
else
{
for (String split : splits)
{
Integer current = andMap.get(split);
if ((current == null)
|| (current.intValue() < rawDrValue))
{
andMap.put(split, rawDrValue);
}
}
}
}
}
}
}
// For each 'or'
// Case 1: A greater or equal DR for any value in the OR
// e.g. 10/good + 5/magic or good = 10/good
// Case 2: A smaller DR for any value in the OR
// e.g. 10/magic or good + 5/good = 10/magic or good; 5/good
// e.g. 10/magic or good or lawful + 5/good = 10/good; 5/magic or good
for (Map.Entry<Object, Integer> me : orMap.entrySet())
{
String origBypass = me.getKey().toString();
Integer reduction = me.getValue();
String[] orValues = OR_PATTERN.split(origBypass);
boolean shouldAdd = true;
for (String orValue : orValues)
{
// See if we already have a value for this type from the 'and'
// processing.
Integer andDR = andMap.get(orValue);
if (andDR != null && andDR >= reduction)
{
shouldAdd = false;
break;
}
}
if (shouldAdd)
{
andMap.put(origBypass, reduction);
}
}
return andMap;
}
/**
* Returns the Damage Reduction String for the Player Character identified
* by the given CharID.
*
* @param id
* The CharID identifying the Player Character for which the
* Damage Reduction should be returned.
* @return the Damage Reduction String for the Player Character identified
* by the given CharID
*/
public String getDRString(CharID id)
{
return getDRString(id, getCachedMap(id));
}
/*
* Weird exposure for TemplateModifier (don't like this)
*
* TODO This really needs to be in the output layer, not in the facets
*/
public String getDRString(CharID id,
Map<DamageReduction, Set<Object>> cachedMap)
{
CaseInsensitiveMap<Integer> map = getDRMap(id, cachedMap);
TreeMapToList<Integer, String> hml = new TreeMapToList<>();
for (Map.Entry<Object, Integer> me : map.entrySet())
{
String key = me.getKey().toString();
int value = me.getValue();
value += (int) bonusCheckingFacet.getBonus(id, "DR", key);
hml.addToListFor(value, key);
}
for (Integer reduction : hml.getKeySet())
{
if (hml.sizeOfListFor(reduction) > 1)
{
Set<String> set = new TreeSet<>();
for (String s : hml.getListFor(reduction))
{
if (!OR_PATTERN.matcher(s).find())
{
hml.removeFromListFor(reduction, s);
set.add(s);
}
}
hml.addToListFor(reduction, StringUtil.join(set, " and "));
}
}
StringBuilder sb = new StringBuilder(40);
boolean needSeparator = false;
for (Integer reduction : hml.getKeySet())
{
Set<String> set = new TreeSet<>();
for (String s : hml.getListFor(reduction))
{
set.add(reduction + "/" + s);
}
if (needSeparator)
{
sb.insert(0, "; ");
}
needSeparator = true;
sb.insert(0, StringUtil.join(set, "; "));
}
return sb.toString();
}
/**
* Gets the Damage Reduction value for the given Damage Reduction key and
* Player Character identified by the given CharID.
*
* @param id
* The CharID identifying the Player Character for which the
* Damage Reduction for the given key will be returned
* @param key
* The key identifying which Damage Reduction value will be
* returned
* @return The Damage Reduction value for the given Damage Reduction key and
* Player Character identified by the given CharID
*/
public Integer getDR(CharID id, String key)
{
return getNonBonusDR(id, key)
+ (int) bonusCheckingFacet.getBonus(id, "DR", key);
}
/**
* Gets the Damage Reduction value, ignoring all bonuses, for the given
* Damage Reduction key and Player Character identified by the given CharID.
*
* @param id
* The CharID identifying the Player Character for which the
* Damage Reduction for the given key will be returned
* @param key
* The key identifying which Damage Reduction value will be
* returned
* @return The Damage Reduction value, ignoring all bonuses, for the given
* Damage Reduction key and Player Character identified by the given
* CharID
*/
private int getNonBonusDR(CharID id, String key)
{
Integer drValue = getDRMap(id, getCachedMap(id)).get(key);
return (drValue == null)? 0 : drValue;
}
public void setPrerequisiteFacet(PrerequisiteFacet prerequisiteFacet)
{
this.prerequisiteFacet = prerequisiteFacet;
}
public void setFormulaResolvingFacet(FormulaResolvingFacet formulaResolvingFacet)
{
this.formulaResolvingFacet = formulaResolvingFacet;
}
public void setBonusCheckingFacet(BonusCheckingFacet bonusCheckingFacet)
{
this.bonusCheckingFacet = bonusCheckingFacet;
}
public void setConsolidationFacet(CDOMObjectConsolidationFacet consolidationFacet)
{
this.consolidationFacet = consolidationFacet;
}
/**
* Initializes the connections for DamageReductionFacet to other facets.
*
* This method is automatically called by the Spring framework during
* initialization of the DamageReductionFacet.
*/
public void init()
{
consolidationFacet.addDataFacetChangeListener(this);
}
}