/*
* Copyright 2008 (C) Tom Parker <thpr@users.sourceforge.net>
* Derived from Skill.java
* Copyright 2001 (C) Bryan McRoberts <merton_monk@yahoo.com>
*
* 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
*/
package pcgen.core.analysis;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import pcgen.cdom.base.CDOMObjectUtilities;
import pcgen.cdom.base.PersistentTransitionChoice;
import pcgen.cdom.enumeration.AssociationListKey;
import pcgen.cdom.enumeration.ListKey;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.cdom.enumeration.SkillCost;
import pcgen.cdom.enumeration.Type;
import pcgen.core.Globals;
import pcgen.core.Language;
import pcgen.core.PCClass;
import pcgen.core.PlayerCharacter;
import pcgen.core.RuleConstants;
import pcgen.core.Skill;
import pcgen.core.chooser.ChooserUtilities;
import pcgen.core.pclevelinfo.PCLevelInfo;
import pcgen.core.utils.CoreUtility;
import pcgen.util.Logging;
import pcgen.util.enumeration.View;
public class SkillRankControl
{
/**
* Returns the total ranks of a skill rank + bonus ranks (racial, class, etc
* bonuses). Note that the total ranks could be higher than the max ranks if
* the ranks come from a familiar's master.
*
* @param pc
* @return rank + bonus ranks (racial, class, etc. bonuses)
*/
public static Float getTotalRank(PlayerCharacter pc, Skill sk)
{
if (pc == null)
{
Logging.errorPrint("Asked to get total rank for null character. Location was ", new Throwable());
return 0.0f;
}
Float rank = pc.getRank(sk);
if (rank == null)
{
Logging.errorPrint("Rank of skill " + sk + " for " + pc + " was null. Location was ", new Throwable());
return 0.0f;
}
double baseRanks = rank.doubleValue();
double ranks = baseRanks + SkillRankControl.getSkillRankBonusTo(pc, sk);
if (!Globals.checkRule(RuleConstants.SKILLMAX) && pc.hasClass())
{
/*
* Note: The class grabbed doesn't matter - it is only used for calculating cross-class skill rank cost.
* All classes of a multi-class character are scanned to determine if the skill is a class skill.
*/
double maxRanks = pc.getMaxRank(sk,
pc.getClassList().get(0)).doubleValue();
maxRanks = Math.max(maxRanks, baseRanks);
ranks = Math.min(maxRanks, ranks);
}
return new Float(ranks);
}
/**
* Set the ranks for the specified class to zero
*
* @param aClass
* @param aPC
*/
public static void setZeroRanks(PCClass aClass, PlayerCharacter aPC,
Skill sk)
{
if (aClass == null)
{
return;
}
Double rank = aPC.getSkillRankForClass(sk, aClass);
if (rank != null)
{
aPC.removeSkillRankValue(sk, aClass);
String aResp = modRanks(-rank, aClass, false, aPC, sk);
if (!aResp.isEmpty())
{
// error or debug? XXX
Logging.debugPrint(aResp);
}
}
}
/**
* Modify the rank
*
* @param rankMod
* @param aClass
* @param ignorePrereqs
* @param aPC
* @return message
*/
public static String modRanks(double rankMod, PCClass aClass,
boolean ignorePrereqs, PlayerCharacter aPC, Skill sk)
{
int i = 0;
if (!ignorePrereqs)
{
if (aClass == null)
{
return "You must be at least level one before you can purchase skills.";
}
if ((rankMod > 0.0) && !sk.qualifies(aPC, sk))
{
return "You do not meet the prerequisites required to take this skill.";
}
SkillCost sc = aPC.getSkillCostForClass(sk, aClass);
i = sc.getCost();
if (i == 0)
{
return "You cannot purchase this exclusive skill.";
}
if ((rankMod > 0.0) && (aClass.getSkillPool(aPC) < (rankMod * i)))
{
return "You do not have enough skill points.";
}
double maxRank = aPC.getMaxRank(sk, aClass)
.doubleValue();
if (!Globals.checkRule(RuleConstants.SKILLMAX) && (rankMod > 0.0))
{
double ttlRank = getTotalRank(aPC, sk).doubleValue();
if (ttlRank >= maxRank)
{
return "Skill rank at maximum (" + maxRank
+ ") for your level.";
}
if ((ttlRank + rankMod) > maxRank)
{
return "Raising skill would make it above maximum ("
+ maxRank + ") for your level.";
}
}
}
if ((aPC.getRank(sk).doubleValue() + rankMod) < 0.0)
{
return "Cannot lower rank below 0";
}
String classKey = "None";
if (aClass != null)
{
classKey = aClass.getKeyName();
}
Double rank = aPC.getSkillRankForClass(sk, aClass);
double currentRank = (rank == null) ? 0.0 : rank;
if (CoreUtility.doublesEqual(currentRank, 0.0) && (rankMod < 0.0))
{
return "No more ranks found for class: " + classKey
+ ". Try a different one.";
}
rankMod = modRanks2(rankMod, currentRank, aClass, aPC, sk);
if (!ignorePrereqs)
{
if (aClass != null)
{
aPC.setSkillPool(aClass, aClass.getSkillPool(aPC)
- (int) (i * rankMod));
}
}
return "";
}
public static void replaceClassRank(PlayerCharacter pc, Skill sk,
PCClass oldClass, PCClass newClass)
{
Double rank = pc.getSkillRankForLocalClass(sk, oldClass);
if (rank != null)
{
pc.removeSkillRankForLocalClass(sk, oldClass);
pc.setSkillRankValue(sk, newClass, rank);
}
}
private static double modRanks2(double rankChange, double curRank,
PCClass pcc, PlayerCharacter aPC, Skill sk)
{
double newRank = curRank + rankChange;
//
// Modify for the chosen class
//
if (CoreUtility.doublesEqual(newRank, 0.0))
{
aPC.removeSkillRankValue(sk, pcc);
}
else
{
aPC.setSkillRankValue(sk, pcc, newRank);
}
if (!aPC.isImporting())
{
if (ChooseActivation.hasNewChooseToken(sk))
{
if (!CoreUtility.doublesEqual(rankChange, 0)
&& !CoreUtility.doublesEqual(curRank, (int) newRank))
{
ChooserUtilities.modChoices(sk, new ArrayList<Language>(),
new ArrayList<Language>(), aPC, true, null);
aPC.setDirty(true);
int selectedLanguages = aPC
.getSelectCorrectedAssociationCount(sk);
int maxLanguages = getTotalRank(aPC, sk).intValue();
if (selectedLanguages > maxLanguages)
{
newRank = curRank;
}
}
}
}
aPC.calcActiveBonuses();
return rankChange;
}
/**
* Get the bonus to a skill rank
*
* @param aPC
* @return bonus to skill rank
*/
public static double getSkillRankBonusTo(PlayerCharacter aPC, Skill sk)
{
double bonus = aPC.getTotalBonusTo("SKILLRANK", sk.getKeyName());
for (Type singleType : sk.getTrueTypeList(false))
{
bonus += aPC.getTotalBonusTo("SKILLRANK", "TYPE." + singleType);
}
updateAdds(aPC, sk, bonus);
return bonus;
}
private static void updateAdds(PlayerCharacter aPC, Skill sk, double bonus)
{
// Check for ADDs
List<PersistentTransitionChoice<?>> adds = sk.getListFor(ListKey.ADD);
if (adds != null)
{
int iCount = 0;
for (PersistentTransitionChoice<?> ptc : adds)
{
iCount += aPC.getAssocCount(ptc, AssociationListKey.ADD);
}
if (CoreUtility.doublesEqual(aPC.getRank(sk).doubleValue() + bonus,
0.0))
{
//
// There was a total (because we've applied the ADD's, but now
// there isn't.
// Need to remove the ADDed items
//
if (iCount != 0)
{
CDOMObjectUtilities.removeAdds(sk, aPC);
CDOMObjectUtilities.restoreRemovals(sk, aPC);
}
}
else
{
//
// There wasn't a total (because we haven't applied the ADDs),
// but now there is
// Need to apply the ADDed items
//
if (iCount == 0)
{
CDOMObjectUtilities.addAdds(sk, aPC);
CDOMObjectUtilities.checkRemovals(sk, aPC);
}
}
}
}
/**
* Adjust the character's skills to reflect the loss of a level. This is
* done by:
* <ol>
* <li>Removing ranks from any skills with max ranks or more</li>
* <li>Taking any skill points still needing to be refunded off the remainder
* for the next highest level of the class</li>
* <li>or taking them off the remainder for the highest class level.</li>
* </ol>
*
* @param pc The character being updated.
* @param classBeingLevelledDown The class we are removing a level of.
* @param currentLevel The character;s level before the removal.
* @param pointsToRemove The number of points that need to be refunded.
*/
public static void removeSkillsForTopLevel(PlayerCharacter pc,
PCClass classBeingLevelledDown, int currentLevel, int pointsToRemove)
{
double remaining = pointsToRemove;
if (remaining <= 0.0)
{
return;
}
// Remove a rank from each skill with max ranks at the old level (now above max ranks)
for (Skill skill : pc.getSkillSet())
{
if (!skill.getSafe(ObjectKey.VISIBILITY).isVisibleTo(View.VISIBLE_DISPLAY))
{
continue;
}
PCClass aClass = pc.getClassList().isEmpty() ? null : pc.getClassList().get(0);
double maxRanks = pc.getMaxRank(skill, aClass).doubleValue();
double rankMod = maxRanks - getTotalRank(pc, skill);
if (rankMod < 0)
{
if (Logging.isLoggable(Logging.INFO))
{
Logging.log(Logging.INFO, "Removing " + (rankMod*-1) + " ranks from " + skill);
}
String err = modRanks(rankMod, classBeingLevelledDown, true, pc, skill);
if (StringUtils.isBlank(err))
{
remaining += rankMod;
if (remaining <= 0)
{
break;
}
}
}
}
// Deal with any remaining skill points to be refunded
if (remaining != 0.0)
{
// If there are other levels left of the class being removed,
// subtract the remainder from the remaining value of the next
// highest level of the class removed.
PCLevelInfo targetLevel = null;
int level = currentLevel-1;
while (level > 0)
{
PCLevelInfo pcLI = pc.getLevelInfo(level-1);
if (pcLI.getClassKeyName().equals(classBeingLevelledDown.getKeyName()))
{
targetLevel = pcLI;
break;
}
level--;
}
if (targetLevel == null && currentLevel > 0)
{
// Otherwise subtract the remainder from the remaining value of
// the character's highest remaining level.
targetLevel = pc.getLevelInfo(currentLevel-2);
}
if (targetLevel != null)
{
targetLevel.setSkillPointsRemaining(targetLevel
.getSkillPointsRemaining() - (int)remaining);
}
}
}
}