diff --git a/common/ruletypes.h b/common/ruletypes.h index 0d7c45edd..759934538 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -676,6 +676,12 @@ RULE_CATEGORY_END() RULE_CATEGORY(AA) RULE_INT(AA, ExpPerPoint, 23976503) //Amount of exp per AA. Is the same as the amount of exp to go from level 51 to level 52. RULE_BOOL(AA, Stacking, true) //Allow AA that belong to the same group to stack on SOF+ clients. +RULE_BOOL(AA, NormalizedAAEnabled, false) // TSS+ change to AA that normalizes AA XP to a fixed # of white con kills independent of level. +RULE_INT(AA, NormalizedAANumberOfWhiteConPerAA, 25) // The number of white con kills per AA point. +RULE_BOOL(AA, ModernAAScalingEnabled, false) // Are we linearly scaling AA XP based on total # of earned AA? +RULE_REAL(AA, ModernAAScalingStartPercent, 1000) // 1000% or 10x AA XP at the start of the scaling range +RULE_INT(AA, ModernAAScalingAAMinimum, 0) // The minimum number of earned AA before AA XP scaling begins. +RULE_INT(AA, ModernAAScalingAALimit, 4000) // The number of earned AA when AA XP scaling ends RULE_CATEGORY_END() RULE_CATEGORY(Console) diff --git a/zone/client.h b/zone/client.h index 88622fcff..77a76fc18 100644 --- a/zone/client.h +++ b/zone/client.h @@ -606,6 +606,10 @@ public: uint32 GetExperienceForKill(Mob *against); void AddEXP(uint32 in_add_exp, uint8 conlevel = 0xFF, bool resexp = false); uint32 CalcEXP(uint8 conlevel = 0xFF); + void CalculateNormalizedAAExp(uint32 &add_aaxp, uint8 conlevel, bool resexp); + void CalculateStandardAAExp(uint32 &add_aaxp, uint8 conlevel, bool resexp); + void CalculateLeadershipExp(uint32 &add_exp, uint8 conlevel); + void CalculateExp(uint32 in_add_exp, uint32 &add_exp, uint32 &add_aaxp, uint8 conlevel, bool resexp); void SetEXP(uint32 set_exp, uint32 set_aaxp, bool resexp=false); void AddLevelBasedExp(uint8 exp_percentage, uint8 max_level=0); void SetLeadershipEXP(uint32 group_exp, uint32 raid_exp); diff --git a/zone/exp.cpp b/zone/exp.cpp index 1fd8ea0a3..be9d01c13 100644 --- a/zone/exp.cpp +++ b/zone/exp.cpp @@ -37,6 +37,49 @@ extern QueryServ* QServ; +static uint32 ScaleAAXPBasedOnCurrentAATotal(int earnedAA, uint32 add_aaxp) +{ + float baseModifier = RuleR(AA, ModernAAScalingStartPercent); + int aaMinimum = RuleI(AA, ModernAAScalingAAMinimum); + int aaLimit = RuleI(AA, ModernAAScalingAALimit); + + // Are we within the scaling window? + if (earnedAA >= aaLimit || earnedAA < aaMinimum) + { + Log(Logs::Detail, Logs::None, "Not within AA scaling window."); + + // At or past the limit. We're done. + return add_aaxp; + } + + // We're not at the limit yet. How close are we? + int remainingAA = aaLimit - earnedAA; + + // We might not always be X - 0 + int scaleRange = aaLimit - aaMinimum; + + // Normalize and get the effectiveness based on the range and the character's + // current spent AA. + float normalizedScale = (float)remainingAA / scaleRange; + + // Scale. + uint32 totalWithExpMod = add_aaxp * (baseModifier / 100) * normalizedScale; + + // Are we so close to the scale limit that we're earning more XP without scaling? This + // will happen when we get very close to the limit. In this case, just grant the unscaled + // amount. + if (totalWithExpMod < add_aaxp) + { + return add_aaxp; + } + + Log(Logs::Detail, + Logs::None, + "Total before the modifier %d :: NewTotal %d :: ScaleRange: %d, SpentAA: %d, RemainingAA: %d, normalizedScale: %0.3f", + add_aaxp, totalWithExpMod, scaleRange, earnedAA, remainingAA, normalizedScale); + + return totalWithExpMod; +} static uint32 MaxBankedGroupLeadershipPoints(int Level) { @@ -174,192 +217,302 @@ uint32 Client::GetExperienceForKill(Mob *against) return 0; } -void Client::AddEXP(uint32 in_add_exp, uint8 conlevel, bool resexp) { +float static GetConLevelModifierPercent(uint8 conlevel) +{ + switch (conlevel) + { + case CON_GREEN: + return (float)RuleI(Character, GreenModifier) / 100; + break; + case CON_LIGHTBLUE: + return (float)RuleI(Character, LightBlueModifier) / 100; + break; + case CON_BLUE: + return (float)RuleI(Character, BlueModifier) / 100; + break; + case CON_WHITE: + return (float)RuleI(Character, WhiteModifier) / 100; + break; + case CON_YELLOW: + return (float)RuleI(Character, YellowModifier) / 100; + break; + case CON_RED: + return (float)RuleI(Character, RedModifier) / 100; + break; + default: + return 0; + } +} - this->EVENT_ITEM_ScriptStopReturn(); - - uint32 add_exp = in_add_exp; - - if(!resexp && (XPRate != 0)) - add_exp = static_cast(in_add_exp * (static_cast(XPRate) / 100.0f)); - - if (m_epp.perAA<0 || m_epp.perAA>100) - m_epp.perAA=0; // stop exploit with sanity check - - uint32 add_aaxp; - if(resexp) { +void Client::CalculateNormalizedAAExp(uint32 &add_aaxp, uint8 conlevel, bool resexp) +{ + // Functionally this is the same as having the case in the switch, but this is + // cleaner to read. + if (CON_GRAY == conlevel || resexp) + { add_aaxp = 0; - } else { + return; + } + // For this, we ignore the provided value of add_aaxp because it doesn't + // apply. XP per AA is normalized such that there are X white con kills + // per AA. + + uint32 whiteConKillsPerAA = RuleI(AA, NormalizedAANumberOfWhiteConPerAA); + uint32 xpPerAA = RuleI(AA, ExpPerPoint); + + float colorModifier = GetConLevelModifierPercent(conlevel); + float percentToAAXp = (float)m_epp.perAA / 100; + + // Normalize the amount of AA XP we earned for this kill. + add_aaxp = percentToAAXp * (xpPerAA / (whiteConKillsPerAA / colorModifier)); +} + +void Client::CalculateStandardAAExp(uint32 &add_aaxp, uint8 conlevel, bool resexp) +{ + if (!resexp) + { + //if XP scaling is based on the con of a monster, do that now. + if (RuleB(Character, UseXPConScaling)) + { + if (conlevel != 0xFF && !resexp) + { + add_aaxp *= GetConLevelModifierPercent(conlevel); + } + } + } //end !resexp + + float aatotalmod = 1.0; + if (zone->newzone_data.zone_exp_multiplier >= 0) { + aatotalmod *= zone->newzone_data.zone_exp_multiplier; + } + + // Shouldn't race not affect AA XP? + if (RuleB(Character, UseRaceClassExpBonuses)) + { + if (GetBaseRace() == HALFLING) { + aatotalmod *= 1.05; + } + + if (GetClass() == ROGUE || GetClass() == WARRIOR) { + aatotalmod *= 1.05; + } + } + + // why wasn't this here? Where should it be? + if (zone->IsHotzone()) + { + aatotalmod += RuleR(Zone, HotZoneBonus); + } + + if (RuleB(Zone, LevelBasedEXPMods)) { + if (zone->level_exp_mod[GetLevel()].ExpMod) { + add_aaxp *= zone->level_exp_mod[GetLevel()].AAExpMod; + } + } + + add_aaxp = (uint32)(RuleR(Character, AAExpMultiplier) * add_aaxp * aatotalmod); +} + +void Client::CalculateLeadershipExp(uint32 &add_exp, uint8 conlevel) +{ + if (IsLeadershipEXPOn() && (conlevel == CON_BLUE || conlevel == CON_WHITE || conlevel == CON_YELLOW || conlevel == CON_RED)) + { + add_exp = static_cast(static_cast(add_exp) * 0.8f); + + if (GetGroup()) + { + if (m_pp.group_leadership_points < MaxBankedGroupLeadershipPoints(GetLevel()) + && RuleI(Character, KillsPerGroupLeadershipAA) > 0) + { + uint32 exp = GROUP_EXP_PER_POINT / RuleI(Character, KillsPerGroupLeadershipAA); + Client *mentoree = GetGroup()->GetMentoree(); + if (GetGroup()->GetMentorPercent() && mentoree && + mentoree->GetGroupPoints() < MaxBankedGroupLeadershipPoints(mentoree->GetLevel())) + { + uint32 mentor_exp = exp * (GetGroup()->GetMentorPercent() / 100.0f); + exp -= mentor_exp; + mentoree->AddLeadershipEXP(mentor_exp, 0); // ends up rounded down + mentoree->Message_StringID(MT_Leadership, GAIN_GROUP_LEADERSHIP_EXP); + } + if (exp > 0) + { + // possible if you mentor 100% to the other client + AddLeadershipEXP(exp, 0); // ends up rounded up if mentored, no idea how live actually does it + Message_StringID(MT_Leadership, GAIN_GROUP_LEADERSHIP_EXP); + } + } + else + { + Message_StringID(MT_Leadership, MAX_GROUP_LEADERSHIP_POINTS); + } + } + else + { + Raid *raid = GetRaid(); + // Raid leaders CAN NOT gain group AA XP, other group leaders can though! + if (raid->IsLeader(this)) + { + if (m_pp.raid_leadership_points < MaxBankedRaidLeadershipPoints(GetLevel()) + && RuleI(Character, KillsPerRaidLeadershipAA) > 0) + { + AddLeadershipEXP(0, RAID_EXP_PER_POINT / RuleI(Character, KillsPerRaidLeadershipAA)); + Message_StringID(MT_Leadership, GAIN_RAID_LEADERSHIP_EXP); + } + else + { + Message_StringID(MT_Leadership, MAX_RAID_LEADERSHIP_POINTS); + } + } + else + { + if (m_pp.group_leadership_points < MaxBankedGroupLeadershipPoints(GetLevel()) + && RuleI(Character, KillsPerGroupLeadershipAA) > 0) + { + uint32 group_id = raid->GetGroup(this); + uint32 exp = GROUP_EXP_PER_POINT / RuleI(Character, KillsPerGroupLeadershipAA); + Client *mentoree = raid->GetMentoree(group_id); + if (raid->GetMentorPercent(group_id) && mentoree && + mentoree->GetGroupPoints() < MaxBankedGroupLeadershipPoints(mentoree->GetLevel())) + { + uint32 mentor_exp = exp * (raid->GetMentorPercent(group_id) / 100.0f); + exp -= mentor_exp; + mentoree->AddLeadershipEXP(mentor_exp, 0); + mentoree->Message_StringID(MT_Leadership, GAIN_GROUP_LEADERSHIP_EXP); + } + if (exp > 0) + { + AddLeadershipEXP(exp, 0); + Message_StringID(MT_Leadership, GAIN_GROUP_LEADERSHIP_EXP); + } + } + else + { + Message_StringID(MT_Leadership, MAX_GROUP_LEADERSHIP_POINTS); + } + } + } + } +} + +void Client::CalculateExp(uint32 in_add_exp, uint32 &add_exp, uint32 &add_aaxp, uint8 conlevel, bool resexp) +{ + add_exp = in_add_exp; + + if (!resexp && (XPRate != 0)) + { + add_exp = static_cast(in_add_exp * (static_cast(XPRate) / 100.0f)); + } + + // Make sure it was initialized. + add_aaxp = 0; + + if (!resexp) + { //figure out how much of this goes to AAs add_aaxp = add_exp * m_epp.perAA / 100; + //take that amount away from regular exp add_exp -= add_aaxp; float totalmod = 1.0; float zemmod = 1.0; + //get modifiers - if(RuleR(Character, ExpMultiplier) >= 0){ + if (RuleR(Character, ExpMultiplier) >= 0) { totalmod *= RuleR(Character, ExpMultiplier); } - if(zone->newzone_data.zone_exp_multiplier >= 0){ + //add the zone exp modifier. + if (zone->newzone_data.zone_exp_multiplier >= 0) { zemmod *= zone->newzone_data.zone_exp_multiplier; } - if(RuleB(Character,UseRaceClassExpBonuses)) + if (RuleB(Character, UseRaceClassExpBonuses)) { - if(GetBaseRace() == HALFLING){ + if (GetBaseRace() == HALFLING) { totalmod *= 1.05; } - if(GetClass() == ROGUE || GetClass() == WARRIOR){ + if (GetClass() == ROGUE || GetClass() == WARRIOR) { totalmod *= 1.05; } } - if(zone->IsHotzone()) + //add hotzone modifier if one has been set. + if (zone->IsHotzone()) { totalmod += RuleR(Zone, HotZoneBonus); } add_exp = uint32(float(add_exp) * totalmod * zemmod); - if(RuleB(Character,UseXPConScaling)) + //if XP scaling is based on the con of a monster, do that now. + if (RuleB(Character, UseXPConScaling)) { - if (conlevel != 0xFF && !resexp) { - switch (conlevel) - { - case CON_GRAY: - add_exp = 0; - add_aaxp = 0; - return; - case CON_GREEN: - add_exp = add_exp * RuleI(Character, GreenModifier) / 100; - add_aaxp = add_aaxp * RuleI(Character, GreenModifier) / 100; - break; - case CON_LIGHTBLUE: - add_exp = add_exp * RuleI(Character, LightBlueModifier)/100; - add_aaxp = add_aaxp * RuleI(Character, LightBlueModifier)/100; - break; - case CON_BLUE: - add_exp = add_exp * RuleI(Character, BlueModifier)/100; - add_aaxp = add_aaxp * RuleI(Character, BlueModifier)/100; - break; - case CON_WHITE: - add_exp = add_exp * RuleI(Character, WhiteModifier)/100; - add_aaxp = add_aaxp * RuleI(Character, WhiteModifier)/100; - break; - case CON_YELLOW: - add_exp = add_exp * RuleI(Character, YellowModifier)/100; - add_aaxp = add_aaxp * RuleI(Character, YellowModifier)/100; - break; - case CON_RED: - add_exp = add_exp * RuleI(Character, RedModifier)/100; - add_aaxp = add_aaxp * RuleI(Character, RedModifier)/100; - break; - } + if (conlevel != 0xFF && !resexp) + { + add_exp = add_exp * GetConLevelModifierPercent(conlevel); } } - if (IsLeadershipEXPOn() && (conlevel == CON_BLUE || conlevel == CON_WHITE || conlevel == CON_YELLOW || conlevel == CON_RED)) { - add_exp = static_cast(static_cast(add_exp) * 0.8f); - - if (GetGroup()) { - if (m_pp.group_leadership_points < MaxBankedGroupLeadershipPoints(GetLevel()) - && RuleI(Character, KillsPerGroupLeadershipAA) > 0) { - uint32 exp = GROUP_EXP_PER_POINT / RuleI(Character, KillsPerGroupLeadershipAA); - Client *mentoree = GetGroup()->GetMentoree(); - if (GetGroup()->GetMentorPercent() && mentoree && - mentoree->GetGroupPoints() < MaxBankedGroupLeadershipPoints(mentoree->GetLevel())) { - uint32 mentor_exp = exp * (GetGroup()->GetMentorPercent() / 100.0f); - exp -= mentor_exp; - mentoree->AddLeadershipEXP(mentor_exp, 0); // ends up rounded down - mentoree->Message_StringID(MT_Leadership, GAIN_GROUP_LEADERSHIP_EXP); - } - if (exp > 0) { // possible if you mentor 100% to the other client - AddLeadershipEXP(exp, 0); // ends up rounded up if mentored, no idea how live actually does it - Message_StringID(MT_Leadership, GAIN_GROUP_LEADERSHIP_EXP); - } - } else { - Message_StringID(MT_Leadership, MAX_GROUP_LEADERSHIP_POINTS); - } - } else { - Raid *raid = GetRaid(); - // Raid leaders CAN NOT gain group AA XP, other group leaders can though! - if (raid->IsLeader(this)) { - if (m_pp.raid_leadership_points < MaxBankedRaidLeadershipPoints(GetLevel()) - && RuleI(Character, KillsPerRaidLeadershipAA) > 0) { - AddLeadershipEXP(0, RAID_EXP_PER_POINT / RuleI(Character, KillsPerRaidLeadershipAA)); - Message_StringID(MT_Leadership, GAIN_RAID_LEADERSHIP_EXP); - } else { - Message_StringID(MT_Leadership, MAX_RAID_LEADERSHIP_POINTS); - } - } else { - if (m_pp.group_leadership_points < MaxBankedGroupLeadershipPoints(GetLevel()) - && RuleI(Character, KillsPerGroupLeadershipAA) > 0) { - uint32 group_id = raid->GetGroup(this); - uint32 exp = GROUP_EXP_PER_POINT / RuleI(Character, KillsPerGroupLeadershipAA); - Client *mentoree = raid->GetMentoree(group_id); - if (raid->GetMentorPercent(group_id) && mentoree && - mentoree->GetGroupPoints() < MaxBankedGroupLeadershipPoints(mentoree->GetLevel())) { - uint32 mentor_exp = exp * (raid->GetMentorPercent(group_id) / 100.0f); - exp -= mentor_exp; - mentoree->AddLeadershipEXP(mentor_exp, 0); - mentoree->Message_StringID(MT_Leadership, GAIN_GROUP_LEADERSHIP_EXP); - } - if (exp > 0) { - AddLeadershipEXP(exp, 0); - Message_StringID(MT_Leadership, GAIN_GROUP_LEADERSHIP_EXP); - } - } else { - Message_StringID(MT_Leadership, MAX_GROUP_LEADERSHIP_POINTS); - } - } - } - - } - + // Calculate any changes to leadership experience. + CalculateLeadershipExp(add_exp, conlevel); } //end !resexp - float aatotalmod = 1.0; - if(zone->newzone_data.zone_exp_multiplier >= 0){ - aatotalmod *= zone->newzone_data.zone_exp_multiplier; - } - - // Shouldn't race not affect AA XP? - if(RuleB(Character,UseRaceClassExpBonuses)) - { - if(GetBaseRace() == HALFLING){ - aatotalmod *= 1.05; - } - - if(GetClass() == ROGUE || GetClass() == WARRIOR){ - aatotalmod *= 1.05; - } - } - - // why wasn't this here? Where should it be? - if(zone->IsHotzone()) - { - aatotalmod += RuleR(Zone, HotZoneBonus); - } - - if(RuleB(Zone, LevelBasedEXPMods)){ - if(zone->level_exp_mod[GetLevel()].ExpMod){ + if (RuleB(Zone, LevelBasedEXPMods)) { + if (zone->level_exp_mod[GetLevel()].ExpMod) { add_exp *= zone->level_exp_mod[GetLevel()].ExpMod; - add_aaxp *= zone->level_exp_mod[GetLevel()].AAExpMod; } } - uint32 exp = GetEXP() + add_exp; + add_exp = GetEXP() + add_exp; +} - uint32 aaexp = (uint32)(RuleR(Character, AAExpMultiplier) * add_aaxp * aatotalmod); +void Client::AddEXP(uint32 in_add_exp, uint8 conlevel, bool resexp) { + + this->EVENT_ITEM_ScriptStopReturn(); + + uint32 exp = 0; + uint32 aaexp = 0; + + if (m_epp.perAA<0 || m_epp.perAA>100) + m_epp.perAA=0; // stop exploit with sanity check + + // Calculate regular XP + CalculateExp(in_add_exp, exp, aaexp, conlevel, resexp); + + // Calculate regular AA XP + if (!RuleB(AA, NormalizedAAEnabled)) + { + CalculateStandardAAExp(aaexp, conlevel, resexp); + } + else + { + CalculateNormalizedAAExp(aaexp, conlevel, resexp); + } + + // Are we also doing linear AA acceleration? + if (RuleB(AA, ModernAAScalingEnabled) && aaexp > 0) + { + aaexp = ScaleAAXPBasedOnCurrentAATotal(GetAAPoints(), aaexp); + } + + // Get current AA XP total uint32 had_aaexp = GetAAXP(); - aaexp += had_aaexp; - if(aaexp < had_aaexp) - aaexp = had_aaexp; //watch for wrap + // Add it to the XP we just earned. + aaexp += had_aaexp; + + // Make sure our new total (existing + just earned) isn't lower than the + // existing total. If it is, we overflowed the bounds of uint32 and wrapped. + // Reset to the existing total. + if (aaexp < had_aaexp) + { + aaexp = had_aaexp; //watch for wrap + } + + // Now update our character's normal and AA xp SetEXP(exp, aaexp, resexp); }