2025-04-03 19:00:46 -04:00
|
|
|
//:: Constants for configuration
|
|
|
|
const int MAX_CHARACTER_LEVEL = 60; //:: Maximum character level
|
|
|
|
const int MAX_PARTY_SIZE = 12; //:: Maximum party size for XP split
|
|
|
|
const int LEVEL_DIFFERENCE_LIMIT = 8; //:: Max level difference within party
|
2025-04-06 17:05:06 -04:00
|
|
|
const int XP_REWARD_CAP = 800; //:: Max per PC reward, after split
|
2025-04-03 19:00:46 -04:00
|
|
|
|
|
|
|
const float BASE_XP_MULTIPIER = 50.0;
|
|
|
|
const float BONUS_XP_MODIFIER = 5.0;
|
|
|
|
const float MAX_CR = 80.0; //:: Maximum challenge rating cap
|
|
|
|
const float HIGH_LEVEL_PENALTY_THRESHOLD = 15.0; //:: Threshold for high-level penalty
|
|
|
|
const float XP_ADJUST_MULTIPLIER = 1.0; //:: Multiplier for XP adjustment
|
|
|
|
|
|
|
|
const float XP_DIVISOR_PC = 1.0;
|
|
|
|
const float XP_DIVISOR_DOMINATED = 0.0;
|
|
|
|
const float XP_DIVISOR_HENCHMAN = 0.5;
|
|
|
|
const float XP_DIVISOR_SUMMONED = 0.0;
|
|
|
|
const float XP_DIVISOR_ANIMALCOMPANION = 0.0;
|
|
|
|
const float XP_DIVISOR_FAMILIAR = 0.0;
|
|
|
|
const float XP_DIVISOR_UNKNOWN = 0.5;
|
|
|
|
|
|
|
|
#include "inc_utility"
|
|
|
|
|
|
|
|
int GetECLMod(object oCreature);
|
|
|
|
|
|
|
|
int GetActualLevel(object oPC);
|
|
|
|
|
|
|
|
void RewardCombatXP(object oKiller, object oVictim = OBJECT_SELF);
|
|
|
|
|
|
|
|
/*
|
|
|
|
Race LA is done entirely through this script. DO NOT set PRC_XP_USE_SIMPLE_LA
|
|
|
|
or the XP penalty will be applied twice
|
|
|
|
*/
|
|
|
|
int GetECLMod(object oCreature)
|
|
|
|
{
|
|
|
|
//:: Note this is not MyPRCGetRacialType becuase we want to include subraces too
|
|
|
|
int nRace = GetRacialType(oCreature);
|
|
|
|
int nLA = 0;
|
|
|
|
nLA = StringToInt(Get2DACache("ECL", "LA", nRace));
|
|
|
|
return nLA;
|
|
|
|
}
|
|
|
|
|
|
|
|
//:: Returns oPC's level based on XP amount, not hit dice.
|
|
|
|
int GetActualLevel(object oPC)
|
|
|
|
{
|
|
|
|
return FloatToInt(0.5 + sqrt(0.25 + ( IntToFloat(GetXP(oPC)) / 500 )));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void RewardCombatXP(object oKiller, object oVictim = OBJECT_SELF)
|
|
|
|
{
|
|
|
|
//:: Ensure Master gets credit for Associate kills
|
|
|
|
if (GetIsObjectValid(oKiller))
|
|
|
|
{
|
|
|
|
object oMaster = GetMaster(oKiller);
|
|
|
|
if (GetIsObjectValid(oMaster))
|
|
|
|
{
|
|
|
|
oKiller = oMaster;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//:: If killed by a trap, reward trap placer
|
|
|
|
if (GetObjectType(oKiller) == OBJECT_TYPE_TRIGGER)
|
|
|
|
oKiller = GetTrapCreator(oKiller);
|
|
|
|
|
|
|
|
//:: Sanity checks
|
|
|
|
if (oKiller == oVictim
|
|
|
|
|| !GetIsObjectValid(oKiller)
|
|
|
|
|| GetIsFriend(oKiller, oVictim))
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
//:: Get challenge rating of oVictim
|
|
|
|
float fCR = GetChallengeRating(oVictim);
|
|
|
|
if (fCR > MAX_CR) fCR = MAX_CR;
|
|
|
|
|
|
|
|
//:: Calculate base and bonus XP
|
|
|
|
float BaseEXP = fCR * BASE_XP_MULTIPIER;
|
|
|
|
float BonusEXP = fCR + BONUS_XP_MODIFIER;
|
|
|
|
|
|
|
|
//:: Variables for party info
|
|
|
|
float PartyLevelSum = 0.0;
|
|
|
|
int NumOfParty = 0;
|
|
|
|
int nNoLeech = 0;
|
|
|
|
float PartyModifier = 0.0; //:: Tracks associate modifier for PartySize
|
|
|
|
|
|
|
|
//:: Iterate over PCs
|
|
|
|
object oPartyMember = GetFirstFactionMember(oKiller, TRUE);
|
|
|
|
while (GetIsObjectValid(oPartyMember))
|
|
|
|
{
|
|
|
|
if (GetArea(oVictim) == GetArea(oPartyMember))
|
|
|
|
{
|
|
|
|
int nTrueLevel = GetActualLevel(oPartyMember) + GetECLMod(oPartyMember);
|
|
|
|
nNoLeech = PRCMax(nNoLeech, nTrueLevel);
|
|
|
|
PartyLevelSum += nTrueLevel;
|
|
|
|
NumOfParty++;
|
|
|
|
PartyModifier += XP_DIVISOR_PC;
|
|
|
|
}
|
|
|
|
oPartyMember = GetNextFactionMember(oPartyMember, TRUE);
|
|
|
|
}
|
|
|
|
|
|
|
|
//:: Iterate over associates
|
|
|
|
oPartyMember = GetFirstFactionMember(oKiller, FALSE);
|
|
|
|
while (GetIsObjectValid(oPartyMember))
|
|
|
|
{
|
|
|
|
if (GetArea(oVictim) == GetArea(oPartyMember))
|
|
|
|
{
|
|
|
|
int nAssociateType = GetAssociateType(oPartyMember);
|
|
|
|
int nTrueLevel = GetActualLevel(oPartyMember) + GetECLMod(oPartyMember);
|
|
|
|
|
|
|
|
switch (nAssociateType)
|
|
|
|
{
|
|
|
|
case ASSOCIATE_TYPE_HENCHMAN:
|
|
|
|
PartyModifier += XP_DIVISOR_HENCHMAN;
|
|
|
|
break;
|
|
|
|
case ASSOCIATE_TYPE_ANIMALCOMPANION:
|
|
|
|
PartyModifier += XP_DIVISOR_ANIMALCOMPANION;
|
|
|
|
break;
|
|
|
|
case ASSOCIATE_TYPE_FAMILIAR:
|
|
|
|
PartyModifier += XP_DIVISOR_FAMILIAR;
|
|
|
|
break;
|
|
|
|
case ASSOCIATE_TYPE_SUMMONED:
|
|
|
|
PartyModifier += XP_DIVISOR_SUMMONED;
|
|
|
|
break;
|
|
|
|
case ASSOCIATE_TYPE_DOMINATED:
|
|
|
|
PartyModifier += XP_DIVISOR_DOMINATED;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
PartyModifier += XP_DIVISOR_UNKNOWN;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
oPartyMember = GetNextFactionMember(oPartyMember, FALSE);
|
|
|
|
}
|
|
|
|
|
|
|
|
//:: Avoid division by zero
|
|
|
|
if (PartyLevelSum <= 1.0) PartyLevelSum = 1.0;
|
|
|
|
if (NumOfParty <= 1) NumOfParty = 1;
|
|
|
|
|
|
|
|
//:: Calculate average party level
|
|
|
|
float PartyAvgLvl = PartyLevelSum / NumOfParty;
|
|
|
|
|
|
|
|
//:: Adjust XP based on challenge rating and party level
|
|
|
|
float AdjustValue = ((fCR / PartyAvgLvl) + 2.0) / 3.0;
|
|
|
|
float FinalMonValue = AdjustValue > 1.0
|
|
|
|
? BaseEXP + (BonusEXP * AdjustValue * XP_ADJUST_MULTIPLIER)
|
|
|
|
: BaseEXP * AdjustValue;
|
|
|
|
|
|
|
|
//:: Apply penalty if party average level is above threshold
|
|
|
|
if (PartyAvgLvl >= HIGH_LEVEL_PENALTY_THRESHOLD)
|
|
|
|
{
|
|
|
|
FinalMonValue -= PartyAvgLvl;
|
|
|
|
}
|
|
|
|
|
|
|
|
//:: Split the XP between the party members
|
|
|
|
if (NumOfParty > MAX_PARTY_SIZE)
|
|
|
|
NumOfParty = MAX_PARTY_SIZE;
|
|
|
|
|
|
|
|
float SplitFinalEXP = FinalMonValue / NumOfParty;
|
|
|
|
if (SplitFinalEXP <= 1.0)
|
|
|
|
SplitFinalEXP = 1.0;
|
|
|
|
|
|
|
|
//:: Add modifier for associates
|
|
|
|
SplitFinalEXP += PartyModifier;
|
|
|
|
|
|
|
|
//:: Distribute XP to party members
|
|
|
|
oPartyMember = GetFirstFactionMember(oKiller, TRUE);
|
|
|
|
while (GetIsObjectValid(oPartyMember))
|
|
|
|
{
|
|
|
|
if (GetArea(oVictim) == GetArea(oPartyMember))
|
|
|
|
{
|
|
|
|
int nHD = GetActualLevel(oPartyMember) + GetECLMod(oPartyMember);
|
|
|
|
if (GetIsDead(oPartyMember))
|
|
|
|
{
|
|
|
|
SendMessageToPC(oPartyMember, "You cannot gain experience while dead.");
|
|
|
|
}
|
|
|
|
else if (nHD <= (nNoLeech - (LEVEL_DIFFERENCE_LIMIT + 1)) ||
|
|
|
|
nHD >= FloatToInt(PartyAvgLvl) + LEVEL_DIFFERENCE_LIMIT)
|
|
|
|
{
|
|
|
|
SendMessageToPC(oPartyMember, "You are too far above or below the average party level to gain experience.");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GiveXPToCreature(oPartyMember, PRCMin(XP_REWARD_CAP, FloatToInt(SplitFinalEXP)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
oPartyMember = GetNextFactionMember(oPartyMember, TRUE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* void RewardCombatXP(object oKiller, object oVictim = OBJECT_SELF)
|
|
|
|
{
|
|
|
|
//:: Ensure Master gets credit for Associate kills
|
|
|
|
if (GetIsObjectValid(oKiller))
|
|
|
|
{
|
|
|
|
object oMaster = GetMaster(oKiller);
|
|
|
|
if (GetIsObjectValid(oMaster))
|
|
|
|
{
|
|
|
|
oKiller = oMaster;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//:: If killed by a trap, reward trap placer.
|
|
|
|
if(GetObjectType(oKiller) == OBJECT_TYPE_TRIGGER)
|
|
|
|
oKiller = GetTrapCreator(oKiller);
|
|
|
|
|
|
|
|
//:: Sanity checks
|
|
|
|
if (oKiller == oVictim
|
|
|
|
|| !GetIsObjectValid(oKiller)
|
|
|
|
|| GetIsFriend(oKiller, oVictim)
|
|
|
|
|| !GetIsObjectValid(GetFirstFactionMember(oKiller, TRUE)))
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
//:: Get challenge rating of oVictim
|
|
|
|
float fCR = GetChallengeRating(oVictim);
|
|
|
|
|
|
|
|
//:: Cap CR to max value
|
|
|
|
if (fCR > MAX_CR) fCR = MAX_CR;
|
|
|
|
|
|
|
|
//:: Calculate base and bonus XP
|
|
|
|
float BaseEXP = fCR * BASE_XP_MULTIPIER;
|
|
|
|
float BonusEXP = fCR + BONUS_XP_MODIFIER;
|
|
|
|
|
|
|
|
//:: Variables for party info
|
|
|
|
float PartyLevelSum = 0.0;
|
|
|
|
int NumOfParty = 0;
|
|
|
|
int nNoLeech = 0;
|
|
|
|
|
|
|
|
float PartyModifier = 0.0; //:: Tracks associate modifier for PartySize
|
|
|
|
|
|
|
|
//:: Iterate over party members to calculate levels, count members, and add modifiers for associates
|
|
|
|
object oPartyMember = GetFirstFactionMember(oKiller, TRUE);
|
|
|
|
while (GetIsObjectValid(oPartyMember))
|
|
|
|
{
|
|
|
|
//:: Check if the member is in the same area as the victim
|
|
|
|
if (GetArea(oVictim) == GetArea(oPartyMember))
|
|
|
|
{
|
|
|
|
int nTrueLevel = GetActualLevel(oPartyMember)+ GetECLMod(oPartyMember);
|
|
|
|
int nLeech = nTrueLevel;
|
|
|
|
nNoLeech = max(nNoLeech, nLeech); //:: Track the highest hit dice for leeching
|
|
|
|
PartyLevelSum += nTrueLevel; //:: Sum up party member levels
|
|
|
|
NumOfParty++;
|
|
|
|
|
|
|
|
//:: Handle associates party size modifier
|
|
|
|
int nAssociateType = GetAssociateType(oPartyMember);
|
|
|
|
switch (nAssociateType)
|
|
|
|
{
|
|
|
|
case 1: //:: Henchman
|
|
|
|
PartyModifier += XP_DIVISOR_HENCHMAN;
|
|
|
|
break;
|
|
|
|
case 2: //:: Animal Companion
|
|
|
|
PartyModifier += XP_DIVISOR_ANIMALCOMPANION;
|
|
|
|
break;
|
|
|
|
case 3: //:: Familiar
|
|
|
|
PartyModifier += XP_DIVISOR_FAMILIAR;
|
|
|
|
break;
|
|
|
|
case 4: //:: Summoned
|
|
|
|
PartyModifier += XP_DIVISOR_SUMMONED;
|
|
|
|
break;
|
|
|
|
case 5: //:: Dominated
|
|
|
|
PartyModifier += XP_DIVISOR_DOMINATED;
|
|
|
|
break;
|
|
|
|
default: //:: Anything else should be a PC
|
|
|
|
PartyModifier += XP_DIVISOR_PC;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
oPartyMember = GetNextFactionMember(oPartyMember, TRUE);
|
|
|
|
}
|
|
|
|
|
|
|
|
//:: Avoid division by zero
|
|
|
|
if (PartyLevelSum <= 1.0) PartyLevelSum = 1.0;
|
|
|
|
if (NumOfParty <= 1) NumOfParty = 1;
|
|
|
|
|
|
|
|
//:: Calculate average party level
|
|
|
|
float PartyAvgLvl = PartyLevelSum / NumOfParty;
|
|
|
|
|
|
|
|
//:: Adjust XP based on challenge rating and party level
|
|
|
|
float AdjustValue = ((fCR / PartyAvgLvl) + 2.0) / 3.0;
|
|
|
|
float FinalMonValue = AdjustValue > 1.0
|
|
|
|
? BaseEXP + (BonusEXP * AdjustValue * XP_ADJUST_MULTIPLIER)
|
|
|
|
: BaseEXP * AdjustValue;
|
|
|
|
|
|
|
|
//:: Apply penalty if party average level is above threshold
|
|
|
|
if (PartyAvgLvl >= HIGH_LEVEL_PENALTY_THRESHOLD)
|
|
|
|
{
|
|
|
|
FinalMonValue -= PartyAvgLvl;
|
|
|
|
}
|
|
|
|
|
|
|
|
//:: Split the XP between the party members
|
|
|
|
if (NumOfParty > MAX_PARTY_SIZE)
|
|
|
|
NumOfParty = MAX_PARTY_SIZE;
|
|
|
|
|
|
|
|
float SplitFinalEXP = FinalMonValue / NumOfParty;
|
|
|
|
if (SplitFinalEXP <= 1.0)
|
|
|
|
SplitFinalEXP = 1.0;
|
|
|
|
|
|
|
|
//:: Add modifier for associates: they don't receive XP but affect the XP split
|
|
|
|
SplitFinalEXP += PartyModifier;
|
|
|
|
|
|
|
|
//:: Calculate party bonus
|
|
|
|
float PartyBonus = ((FinalMonValue - SplitFinalEXP + 1.0) / 1.75)
|
|
|
|
+ (FinalMonValue + ((21.0 - PartyAvgLvl) / 3.0));
|
|
|
|
int SFEint = FloatToInt(PartyBonus);
|
|
|
|
|
|
|
|
//:: Distribute XP to party members
|
|
|
|
oPartyMember = GetFirstFactionMember(oKiller, TRUE);
|
|
|
|
while (GetIsObjectValid(oPartyMember))
|
|
|
|
{
|
|
|
|
if (GetArea(oVictim) == GetArea(oPartyMember))
|
|
|
|
{
|
|
|
|
int nHD = GetActualLevel(oPartyMember)+ GetECLMod(oPartyMember);
|
|
|
|
|
|
|
|
//:: Check conditions for giving XP
|
|
|
|
if (GetIsDead(oPartyMember))
|
|
|
|
{
|
|
|
|
SendMessageToPC(oPartyMember, "You cannot gain experience while dead.");
|
|
|
|
}
|
|
|
|
else if (nHD <= (nNoLeech - (LEVEL_DIFFERENCE_LIMIT + 1)) ||
|
|
|
|
nHD >= FloatToInt(PartyAvgLvl) + LEVEL_DIFFERENCE_LIMIT)
|
|
|
|
{
|
|
|
|
SendMessageToPC(oPartyMember, "You are too far above or below the average party level to gain experience.");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GiveXPToCreature(oPartyMember, min(XP_REWARD_CAP, SFEint)); //:: Give XP to valid party member
|
|
|
|
}
|
|
|
|
}
|
|
|
|
oPartyMember = GetNextFactionMember(oPartyMember, TRUE);
|
|
|
|
}
|
|
|
|
} */
|
|
|
|
|
|
|
|
void main()
|
|
|
|
{
|
|
|
|
object oVictim = OBJECT_SELF;
|
|
|
|
object oKiller = GetLastKiller();
|
|
|
|
|
|
|
|
RewardCombatXP(oKiller, oVictim);
|
|
|
|
}
|