Rework regens to match modern clients

This commit is contained in:
Michael Cook (mackal) 2017-10-08 00:13:53 -04:00
parent 43f459b194
commit 8400994c57
31 changed files with 558 additions and 95 deletions

View File

@ -1,5 +1,14 @@
EQEMu Changelog (Started on Sept 24, 2003 15:50)
-------------------------------------------------------
== 10/08/2017 ==
Mackal: Rework regens
Regen will now match whats reported by modern clients, besides where they lie due to known bugs
HP and END regens are now based on the BaseData.txt values allowing easy customization
Those cases:
- The client always applies hunger penalties, it appears they don't exist anymore on live you can turn them on with a rule
- The way the client gets buff mana/end regen benefits incorrectly applies the bard mod making these values lie sometimes
== 9/17/2017 ==
Akkadius: Add model/race offset to FixZ calc (KLS)

View File

@ -24,8 +24,8 @@ struct BaseDataStruct
double base_hp;
double base_mana;
double base_end;
double unk1;
double unk2;
double hp_regen;
double end_regen;
double hp_factor;
double mana_factor;
double endurance_factor;

View File

@ -854,6 +854,7 @@ static const uint32 MAX_PP_REF_SPELLBOOK = 480; // Set for Player Profile size r
static const uint32 MAX_PP_REF_MEMSPELL = 9; // Set for Player Profile size retain
static const uint32 MAX_PP_SKILL = PACKET_SKILL_ARRAY_SIZE; // 100 - actual skills buffer size
static const uint32 MAX_PP_INNATE_SKILL = 25;
static const uint32 MAX_PP_AA_ARRAY = 240;
static const uint32 MAX_GROUP_MEMBERS = 6;
static const uint32 MAX_RECAST_TYPES = 20;
@ -993,7 +994,8 @@ struct PlayerProfile_Struct
/*4768*/ int32 platinum_shared; // Platinum shared between characters
/*4772*/ uint8 unknown4808[24];
/*4796*/ uint32 skills[MAX_PP_SKILL]; // [400] List of skills // 100 dword buffer
/*5196*/ uint8 unknown5132[184];
/*5196*/ uint32 InnateSkills[MAX_PP_INNATE_SKILL];
/*5296*/ uint8 unknown5132[84];
/*5380*/ uint32 pvp2; //
/*5384*/ uint32 unknown5420; //
/*5388*/ uint32 pvptype; //

View File

@ -2184,11 +2184,11 @@ namespace RoF
outapp->WriteUInt32(emu->skills[r]);
}
outapp->WriteUInt32(25); // Unknown count
outapp->WriteUInt32(structs::MAX_PP_INNATE_SKILL); // Innate Skills count
for (uint32 r = 0; r < 25; r++)
for (uint32 r = 0; r < structs::MAX_PP_INNATE_SKILL; r++)
{
outapp->WriteUInt32(0); // Unknown
outapp->WriteUInt32(emu->InnateSkills[r]); // Innate Skills (regen, slam, etc)
}
outapp->WriteUInt32(structs::MAX_PP_DISCIPLINES); // Discipline count

View File

@ -2261,11 +2261,11 @@ namespace RoF2
outapp->WriteUInt32(emu->skills[r]);
}
outapp->WriteUInt32(25); // Unknown count
outapp->WriteUInt32(structs::MAX_PP_INNATE_SKILL); // Innate Skills count
for (uint32 r = 0; r < 25; r++)
for (uint32 r = 0; r < structs::MAX_PP_INNATE_SKILL; r++)
{
outapp->WriteUInt32(0); // Unknown
outapp->WriteUInt32(emu->InnateSkills[r]); // Innate Skills (regen, slam, etc)
}
outapp->WriteUInt32(structs::MAX_PP_DISCIPLINES); // Discipline count

View File

@ -131,6 +131,7 @@ static const uint32 MAX_PP_LANGUAGE = 32; // was 25
static const uint32 MAX_PP_SPELLBOOK = 720; // was 480
static const uint32 MAX_PP_MEMSPELL = 16; // was 12
static const uint32 MAX_PP_SKILL = PACKET_SKILL_ARRAY_SIZE; // 100 - actual skills buffer size
static const uint32 MAX_PP_INNATE_SKILL = 25;
static const uint32 MAX_PP_AA_ARRAY = 300;
static const uint32 MAX_PP_DISCIPLINES = 300; // was 200
static const uint32 MAX_GROUP_MEMBERS = 6;
@ -1155,8 +1156,8 @@ union
/*01012*/ AA_Array aa_array[MAX_PP_AA_ARRAY]; // [300] 3600 bytes - AAs 12 bytes each
/*04612*/ uint32 skill_count; // Seen 100
/*04616*/ uint32 skills[MAX_PP_SKILL]; // [100] 400 bytes - List of skills
/*05016*/ uint32 unknown15_count; // Seen 25
/*05020*/ uint32 unknown_rof15[25]; // Most are 255 or 0
/*05016*/ uint32 InnateSkills_count; // Seen 25
/*05020*/ uint32 InnateSkills[MAX_PP_INNATE_SKILL]; // Most are 255 or 0
/*05120*/ uint32 discipline_count; // Seen 200
/*05124*/ Disciplines_Struct disciplines; // [200] 800 bytes Known disciplines
/*05924*/ uint32 timestamp_count; // Seen 20

View File

@ -131,6 +131,7 @@ static const uint32 MAX_PP_LANGUAGE = 32; // was 25
static const uint32 MAX_PP_SPELLBOOK = 720; // was 480
static const uint32 MAX_PP_MEMSPELL = 16; // was 12
static const uint32 MAX_PP_SKILL = PACKET_SKILL_ARRAY_SIZE; // 100 - actual skills buffer size
static const uint32 MAX_PP_INNATE_SKILL = 25;
static const uint32 MAX_PP_AA_ARRAY = 300;
static const uint32 MAX_PP_DISCIPLINES = 200; // was 100
static const uint32 MAX_GROUP_MEMBERS = 6;
@ -1096,8 +1097,8 @@ union
/*01012*/ AA_Array aa_array[MAX_PP_AA_ARRAY]; // [300] 3600 bytes - AAs 12 bytes each
/*04612*/ uint32 skill_count; // Seen 100
/*04616*/ uint32 skills[MAX_PP_SKILL]; // [100] 400 bytes - List of skills
/*05016*/ uint32 unknown15_count; // Seen 25
/*05020*/ uint32 unknown_rof15[25]; // Most are 255 or 0
/*05016*/ uint32 InnateSkills_count; // Seen 25
/*05020*/ uint32 InnateSkills[MAX_PP_INNATE_SKILL]; // Most are 255 or 0
/*05120*/ uint32 discipline_count; // Seen 200
/*05124*/ Disciplines_Struct disciplines; // [200] 800 bytes Known disciplines
/*05924*/ uint32 timestamp_count; // Seen 20

View File

@ -1605,6 +1605,7 @@ namespace SoD
OUT(copper_cursor);
OUT_array(skills, structs::MAX_PP_SKILL); // 1:1 direct copy (100 dword)
OUT_array(InnateSkills, structs::MAX_PP_INNATE_SKILL); // 1:1 direct copy (25 dword)
// OUT(unknown04760[236]);
OUT(toxicity);

View File

@ -818,6 +818,7 @@ static const uint32 MAX_PP_LANGUAGE = 25; //
static const uint32 MAX_PP_SPELLBOOK = 480; // Confirmed 60 pages on Live now
static const uint32 MAX_PP_MEMSPELL = 10; //was 9 now 10 on Live
static const uint32 MAX_PP_SKILL = PACKET_SKILL_ARRAY_SIZE; // 100 - actual skills buffer size
static const uint32 MAX_PP_INNATE_SKILL = 25;
static const uint32 MAX_PP_AA_ARRAY = 300; //was 299
static const uint32 MAX_GROUP_MEMBERS = 6;
static const uint32 MAX_RECAST_TYPES = 20;
@ -923,7 +924,8 @@ struct PlayerProfile_Struct
/*06488*/ uint32 silver_cursor; // Silver Pieces on cursor
/*06492*/ uint32 copper_cursor; // Copper Pieces on cursor
/*06496*/ uint32 skills[MAX_PP_SKILL]; // [400] List of skills // 100 dword buffer
/*06896*/ uint8 unknown04760[136];
/*06896*/ uint32 InnateSkills[MAX_PP_SKILL];
/*06996*/ uint8 unknown04760[36];
/*07032*/ uint32 toxicity; // Potion Toxicity (15=too toxic, each potion adds 3)
/*07036*/ uint32 thirst_level; // Drink (ticks till next drink)
/*07040*/ uint32 hunger_level; // Food (ticks till next eat)

View File

@ -1276,6 +1276,7 @@ namespace SoF
OUT(copper_cursor);
OUT_array(skills, structs::MAX_PP_SKILL); // 1:1 direct copy (100 dword)
OUT_array(InnateSkills, structs::MAX_PP_INNATE_SKILL); // 1:1 direct copy (25 dword)
// OUT(unknown04760[236]);
OUT(toxicity);

View File

@ -799,6 +799,7 @@ static const uint32 MAX_PP_LANGUAGE = 25; //
static const uint32 MAX_PP_SPELLBOOK = 480; // Confirmed 60 pages on Live now
static const uint32 MAX_PP_MEMSPELL = 10; //was 9 now 10 on Live
static const uint32 MAX_PP_SKILL = PACKET_SKILL_ARRAY_SIZE; // 100 - actual skills buffer size
static const uint32 MAX_PP_INNATE_SKILL = 25;
static const uint32 MAX_PP_AA_ARRAY = 300; //was 299
static const uint32 MAX_GROUP_MEMBERS = 6;
static const uint32 MAX_RECAST_TYPES = 20;
@ -903,7 +904,8 @@ struct PlayerProfile_Struct //23576 Octets
/*06488*/ uint32 silver_cursor; // Silver Pieces on cursor
/*06492*/ uint32 copper_cursor; // Copper Pieces on cursor
/*06496*/ uint32 skills[MAX_PP_SKILL]; // [400] List of skills // 100 dword buffer
/*06896*/ uint8 unknown04760[136];
/*06896*/ uint32 InnateSkills[MAX_PP_INNATE_SKILL];
/*06996*/ uint8 unknown04760[36];
/*07032*/ uint32 toxicity; // Potion Toxicity (15=too toxic, each potion adds 3)
/*07036*/ uint32 thirst_level; // Drink (ticks till next drink)
/*07040*/ uint32 hunger_level; // Food (ticks till next eat)

View File

@ -1020,6 +1020,7 @@ namespace Titanium
OUT(copper_cursor);
OUT_array(skills, structs::MAX_PP_SKILL); // 1:1 direct copy (100 dword)
OUT_array(InnateSkills, structs::MAX_PP_INNATE_SKILL); // 1:1 direct copy (25 dword)
// OUT(unknown04760[236]);
OUT(toxicity);

View File

@ -740,6 +740,7 @@ static const uint32 MAX_PP_LANGUAGE = 28;
static const uint32 MAX_PP_SPELLBOOK = 400;
static const uint32 MAX_PP_MEMSPELL = 9;
static const uint32 MAX_PP_SKILL = PACKET_SKILL_ARRAY_SIZE; // 100 - actual skills buffer size
static const uint32 MAX_PP_INNATE_SKILL = 25;
static const uint32 MAX_PP_AA_ARRAY = 240;
static const uint32 MAX_GROUP_MEMBERS = 6;
static const uint32 MAX_RECAST_TYPES = 20;
@ -844,7 +845,8 @@ struct PlayerProfile_Struct
/*04452*/ uint32 silver_cursor; // Silver Pieces on cursor
/*04456*/ uint32 copper_cursor; // Copper Pieces on cursor
/*04460*/ uint32 skills[MAX_PP_SKILL]; // [400] List of skills // 100 dword buffer
/*04860*/ uint8 unknown04760[136];
/*04860*/ uint32 InnateSkills[MAX_PP_INNATE_SKILL];
/*04960*/ uint8 unknown04760[36];
/*04996*/ uint32 toxicity; // Potion Toxicity (15=too toxic, each potion adds 3)
/*05000*/ uint32 thirst_level; // Drink (ticks till next drink)
/*05004*/ uint32 hunger_level; // Food (ticks till next eat)

View File

@ -1850,6 +1850,7 @@ namespace UF
OUT(copper_cursor);
OUT_array(skills, structs::MAX_PP_SKILL); // 1:1 direct copy (100 dword)
OUT_array(InnateSkills, structs::MAX_PP_INNATE_SKILL); // 1:1 direct copy (25 dword)
// OUT(unknown04760[236]);
OUT(toxicity);

View File

@ -848,6 +848,7 @@ static const uint32 MAX_PP_LANGUAGE = 25; //
static const uint32 MAX_PP_SPELLBOOK = 720; // Confirmed 60 pages on Underfoot now
static const uint32 MAX_PP_MEMSPELL = 12; //was 9 now 10 on Underfoot
static const uint32 MAX_PP_SKILL = PACKET_SKILL_ARRAY_SIZE; // 100 - actual skills buffer size
static const uint32 MAX_PP_INNATE_SKILL = 25;
static const uint32 MAX_PP_AA_ARRAY = 300; //was 299
static const uint32 MAX_GROUP_MEMBERS = 6;
static const uint32 MAX_RECAST_TYPES = 20;
@ -954,7 +955,8 @@ struct PlayerProfile_Struct
/*07336*/ uint32 silver_cursor; // Silver Pieces on cursor
/*07340*/ uint32 copper_cursor; // Copper Pieces on cursor
/*07344*/ uint32 skills[MAX_PP_SKILL]; // [400] List of skills // 100 dword buffer
/*07744*/ uint8 unknown07644[136];
/*07744*/ uint32 InnateSkills[MAX_PP_INNATE_SKILL];
/*07844*/ uint8 unknown07644[36];
/*07880*/ uint32 toxicity; // Potion Toxicity (15=too toxic, each potion adds 3)
/*07884*/ uint32 thirst_level; // Drink (ticks till next drink)
/*07888*/ uint32 hunger_level; // Food (ticks till next eat)

View File

@ -70,7 +70,7 @@ RULE_INT(Character, ConsumptionMultiplier, 100) //item's hunger restored = this
RULE_BOOL(Character, HealOnLevel, false)
RULE_BOOL(Character, FeignKillsPet, false)
RULE_INT(Character, ItemManaRegenCap, 15)
RULE_INT(Character, ItemHealthRegenCap, 35)
RULE_INT(Character, ItemHealthRegenCap, 30)
RULE_INT(Character, ItemDamageShieldCap, 30)
RULE_INT(Character, ItemAccuracyCap, 150)
RULE_INT(Character, ItemAvoidanceCap, 100)
@ -91,10 +91,12 @@ RULE_INT(Character, HasteCap, 100) // Haste cap for non-v3(overhaste) haste.
RULE_INT(Character, SkillUpModifier, 100) //skill ups are at 100%
RULE_BOOL(Character, SharedBankPlat, false) //off by default to prevent duping for now
RULE_BOOL(Character, BindAnywhere, false)
RULE_INT(Character, RestRegenPercent, 0) // Set to >0 to enable rest state bonus HP and mana regen.
RULE_BOOL(Character, RestRegenEnabled, true) // Enable OOC Regen
RULE_INT(Character, RestRegenHP, 180) // seconds until full from 0. this is actually zone setable, but most or all zones are 180
RULE_INT(Character, RestRegenMana, 180) // seconds until full from 0. this is actually zone setable, but most or all zones are 180
RULE_INT(Character, RestRegenEnd, 180) // seconds until full from 0. this is actually zone setable, but most or all zones are 180
RULE_INT(Character, RestRegenTimeToActivate, 30) // Time in seconds for rest state regen to kick in.
RULE_INT(Character, RestRegenRaidTimeToActivate, 300) // Time in seconds for rest state regen to kick in with a raid target.
RULE_BOOL(Character, RestRegenEndurance, false) // Whether rest regen will work for endurance or not.
RULE_INT(Character, KillsPerGroupLeadershipAA, 250) // Number of dark blues or above per Group Leadership AA
RULE_INT(Character, KillsPerRaidLeadershipAA, 250) // Number of dark blues or above per Raid Leadership AA
RULE_INT(Character, MaxFearDurationForPlayerCharacter, 4) //4 tics, each tic calculates every 6 seconds.
@ -118,6 +120,7 @@ RULE_BOOL(Character, EnableXTargetting, true) // Enable Extended Targetting Wind
RULE_BOOL(Character, EnableAggroMeter, true) // Enable Aggro Meter, for users with RoF and later clients.
RULE_BOOL(Character, KeepLevelOverMax, false) // Don't delevel a character that has somehow gone over the level cap
RULE_INT(Character, FoodLossPerUpdate, 32) // How much food/water you lose per stamina update
RULE_BOOL(Character, EnableHungerPenalties, false) // being hungry/thirsty has negative effects -- it does appear normal live servers do not have penalties
RULE_INT(Character, BaseInstrumentSoftCap, 36) // Softcap for instrument mods, 36 commonly referred to as "3.6" as well.
RULE_BOOL(Character, UseSpellFileSongCap, true) // When they removed the AA that increased the cap they removed the above and just use the spell field
RULE_INT(Character, BaseRunSpeedCap, 158) // Base Run Speed Cap, on live it's 158% which will give you a runspeed of 1.580 hard capped to 225.

View File

@ -1817,8 +1817,8 @@ void SharedDatabase::LoadBaseData(void *data, int max_level) {
bd->base_hp = atof(row[2]);
bd->base_mana = atof(row[3]);
bd->base_end = atof(row[4]);
bd->unk1 = atof(row[5]);
bd->unk2 = atof(row[6]);
bd->hp_regen = atof(row[5]);
bd->end_regen = atof(row[6]);
bd->hp_factor = atof(row[7]);
bd->mana_factor = atof(row[8]);
bd->endurance_factor = atof(row[9]);

View File

@ -131,6 +131,11 @@ enum SpellAffectIndex {
SAI_NPC_Special_80 = 80,
SAI_Trap_Lock = 88
};
enum class GlobalGroup {
Lich = 46,
};
enum RESISTTYPE
{
RESIST_NONE = 0,

View File

@ -0,0 +1,3 @@
UPDATE `rule_values` SET `rule_value` = '32' WHERE `rule_name` = 'Character:FoodLossPerUpdate';
UPDATE `rule_values` SET `rule_value` = '30' WHERE `rule_name` = 'Character:ItemHealthRegenCap';

View File

@ -915,7 +915,7 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon)
newbon->GivePetGroupTarget = true;
break;
case SE_ItemHPRegenCapIncrease:
newbon->ItemHPRegenCap = +base1;
newbon->ItemHPRegenCap += base1;
break;
case SE_Ambidexterity:
newbon->Ambidexterity += base1;

View File

@ -6727,7 +6727,7 @@ int32 Bot::CalcATK() {
}
void Bot::CalcRestState() {
if(!RuleI(Character, RestRegenPercent))
if(!RuleB(Character, RestRegenEnabled))
return;
RestRegenHP = RestRegenMana = RestRegenEndurance = 0;
@ -6743,10 +6743,9 @@ void Bot::CalcRestState() {
}
}
RestRegenHP = (GetMaxHP() * RuleI(Character, RestRegenPercent) / 100);
RestRegenMana = (GetMaxMana() * RuleI(Character, RestRegenPercent) / 100);
if(RuleB(Character, RestRegenEndurance))
RestRegenEndurance = (GetMaxEndurance() * RuleI(Character, RestRegenPercent) / 100);
RestRegenHP = 6 * (GetMaxHP() / RuleI(Character, RestRegenHP));
RestRegenMana = 6 * (GetMaxMana() / RuleI(Character, RestRegenMana));
RestRegenEndurance = 6 * (GetMaxEndurance() / RuleI(Character, RestRegenEnd));
}
int32 Bot::LevelRegen() {

View File

@ -160,7 +160,8 @@ Client::Client(EQStreamInterface* ieqs)
npc_close_scan_timer(6000),
hp_self_update_throttle_timer(300),
hp_other_update_throttle_timer(500),
position_update_timer(10000)
position_update_timer(10000),
tmSitting(0)
{
for (int client_filter = 0; client_filter < _FilterCount; client_filter++)
@ -270,9 +271,10 @@ Client::Client(EQStreamInterface* ieqs)
m_ClientVersion = EQEmu::versions::ClientVersion::Unknown;
m_ClientVersionBit = 0;
AggroCount = 0;
RestRegenHP = 0;
RestRegenMana = 0;
RestRegenEndurance = 0;
ooc_regen = false;
AreaHPRegen = 1.0f;
AreaManaRegen = 1.0f;
AreaEndRegen = 1.0f;
XPRate = 100;
current_endurance = 0;
@ -329,6 +331,9 @@ Client::Client(EQStreamInterface* ieqs)
interrogateinv_flag = false;
for (int i = 0; i < InnateSkillMax; ++i)
m_pp.InnateSkills[i] = InnateDisabled;
AI_Init();
}
@ -4598,7 +4603,7 @@ void Client::IncrementAggroCount() {
//
AggroCount++;
if(!RuleI(Character, RestRegenPercent))
if(!RuleB(Character, RestRegenEnabled))
return;
// If we already had aggro before this method was called, the combat indicator should already be up for SoF clients,
@ -4635,7 +4640,7 @@ void Client::DecrementAggroCount() {
AggroCount--;
if(!RuleI(Character, RestRegenPercent))
if(!RuleB(Character, RestRegenEnabled))
return;
// Something else is still aggro on us, can't rest yet.
@ -6722,7 +6727,7 @@ void Client::SendStatsWindow(Client* client, bool use_window)
cap_regen_field = itoa(CalcHPRegenCap());
spell_regen_field = itoa(spellbonuses.HPRegen);
aa_regen_field = itoa(aabonuses.HPRegen);
total_regen_field = itoa(CalcHPRegen());
total_regen_field = itoa(CalcHPRegen(true));
break;
}
case 1: {
@ -6735,7 +6740,7 @@ void Client::SendStatsWindow(Client* client, bool use_window)
cap_regen_field = itoa(CalcManaRegenCap());
spell_regen_field = itoa(spellbonuses.ManaRegen);
aa_regen_field = itoa(aabonuses.ManaRegen);
total_regen_field = itoa(CalcManaRegen());
total_regen_field = itoa(CalcManaRegen(true));
}
else { continue; }
break;
@ -6749,7 +6754,7 @@ void Client::SendStatsWindow(Client* client, bool use_window)
cap_regen_field = itoa(CalcEnduranceRegenCap());
spell_regen_field = itoa(spellbonuses.EnduranceRegen);
aa_regen_field = itoa(aabonuses.EnduranceRegen);
total_regen_field = itoa(CalcEnduranceRegen());
total_regen_field = itoa(CalcEnduranceRegen(true));
break;
}
default: { break; }
@ -9066,3 +9071,196 @@ void Client::SetPetCommandState(int button, int state)
FastQueuePacket(&app);
}
bool Client::CanMedOnHorse()
{
// no horse is false
if (GetHorseId() == 0)
return false;
// can't med while attacking
if (auto_attack)
return false;
return animation == 0 && m_Delta.x == 0.0f && m_Delta.y == 0.0f; // TODO: animation is SpeedRun
}
void Client::EnableAreaHPRegen(int value)
{
AreaHPRegen = value * 0.001f;
SendAppearancePacket(AT_AreaHPRegen, value); // does this send to whole zone?
// send test and particles?
}
void Client::DisableAreaHPRegen()
{
AreaHPRegen = 1.0f;
SendAppearancePacket(AT_AreaHPRegen, 1000);
}
void Client::EnableAreaManaRegen(int value)
{
AreaManaRegen = value * 0.001f;
SendAppearancePacket(AT_AreaManaRegen, value); // does this send to whole zone?
// send test and particles?
}
void Client::DisableAreaManaRegen()
{
AreaManaRegen = 1.0f;
SendAppearancePacket(AT_AreaManaRegen, 1000);
}
void Client::EnableAreaEndRegen(int value)
{
AreaEndRegen = value * 0.001f;
SendAppearancePacket(AT_AreaEndRegen, value); // does this send to whole zone?
// send test and particles?
}
void Client::DisableAreaEndRegen()
{
AreaEndRegen = 1.0f;
SendAppearancePacket(AT_AreaEndRegen, 1000);
}
void Client::EnableAreaRegens(int value)
{
EnableAreaHPRegen(value);
EnableAreaManaRegen(value);
EnableAreaEndRegen(value);
}
void Client::DisableAreaRegens()
{
DisableAreaHPRegen();
DisableAreaManaRegen();
DisableAreaEndRegen();
}
void Client::InitInnates()
{
// this function on the client also inits the level one innate skills (like swimming, hide, etc)
// we won't do that here, lets just do the InnateSkills for now. Basically translation of what the client is doing
// A lot of these we could probably have ignored because they have no known use or are 100% client side
// but I figured just in case we'll do them all out
//
// The client calls this in a few places. When you remove a vision buff and in SetHeights, which is called in
// illusions, mounts, and a bunch of other cases. All of the calls to InitInnates are wrapped in restoring regen
// besides the call initializing the first time
auto race = GetRace();
auto class_ = GetClass();
for (int i = 0; i < InnateSkillMax; ++i)
m_pp.InnateSkills[i] = InnateDisabled;
m_pp.InnateSkills[InnateInspect] = InnateEnabled;
m_pp.InnateSkills[InnateOpen] = InnateEnabled;
if (race >= RT_FROGLOK_3) {
if (race == RT_SKELETON_2 || race == RT_FROGLOK_3)
m_pp.InnateSkills[InnateUltraVision] = InnateEnabled;
else
m_pp.InnateSkills[InnateInfravision] = InnateEnabled;
}
switch (race) {
case RT_BARBARIAN:
case RT_BARBARIAN_2:
m_pp.InnateSkills[InnateSlam] = InnateEnabled;
break;
case RT_ERUDITE:
case RT_ERUDITE_2:
m_pp.InnateSkills[InnateLore] = InnateEnabled;
break;
case RT_WOOD_ELF:
case RT_GUARD_3:
m_pp.InnateSkills[InnateInfravision] = InnateEnabled;
break;
case RT_HIGH_ELF:
case RT_GUARD_2:
m_pp.InnateSkills[InnateInfravision] = InnateEnabled;
m_pp.InnateSkills[InnateLore] = InnateEnabled;
break;
case RT_DARK_ELF:
case RT_DARK_ELF_2:
case RT_VAMPIRE_2:
m_pp.InnateSkills[InnateUltraVision] = InnateEnabled;
break;
case RT_TROLL:
case RT_TROLL_2:
m_pp.InnateSkills[InnateRegen] = InnateEnabled;
m_pp.InnateSkills[InnateSlam] = InnateEnabled;
m_pp.InnateSkills[InnateInfravision] = InnateEnabled;
break;
case RT_DWARF:
case RT_DWARF_2:
m_pp.InnateSkills[InnateInfravision] = InnateEnabled;
break;
case RT_OGRE:
case RT_OGRE_2:
m_pp.InnateSkills[InnateInfravision] = InnateEnabled;
m_pp.InnateSkills[InnateSlam] = InnateEnabled;
m_pp.InnateSkills[InnateNoBash] = InnateEnabled;
m_pp.InnateSkills[InnateBashDoor] = InnateEnabled;
break;
case RT_HALFLING:
case RT_HALFLING_2:
m_pp.InnateSkills[InnateInfravision] = InnateEnabled;
break;
case RT_GNOME:
m_pp.InnateSkills[InnateInfravision] = InnateEnabled;
m_pp.InnateSkills[InnateLore] = InnateEnabled;
break;
case RT_IKSAR:
m_pp.InnateSkills[InnateRegen] = InnateEnabled;
m_pp.InnateSkills[InnateInfravision] = InnateEnabled;
break;
case RT_VAH_SHIR:
m_pp.InnateSkills[InnateInfravision] = InnateEnabled;
break;
case RT_FROGLOK_2:
case RT_GHOST:
case RT_GHOUL:
case RT_SKELETON:
case RT_VAMPIRE:
case RT_WILL_O_WISP:
case RT_ZOMBIE:
case RT_SPECTRE:
case RT_GHOST_2:
case RT_GHOST_3:
case RT_DRAGON_2:
case RT_INNORUUK:
m_pp.InnateSkills[InnateUltraVision] = InnateEnabled;
break;
case RT_HUMAN:
case RT_GUARD:
case RT_BEGGAR:
case RT_HUMAN_2:
case RT_HUMAN_3:
case RT_FROGLOK_3: // client does froglok weird, but this should work out fine
break;
default:
m_pp.InnateSkills[InnateInfravision] = InnateEnabled;
break;
}
switch (class_) {
case DRUID:
m_pp.InnateSkills[InnateHarmony] = InnateEnabled;
break;
case BARD:
m_pp.InnateSkills[InnateReveal] = InnateEnabled;
break;
case ROGUE:
m_pp.InnateSkills[InnateSurprise] = InnateEnabled;
m_pp.InnateSkills[InnateReveal] = InnateEnabled;
break;
case RANGER:
m_pp.InnateSkills[InnateAwareness] = InnateEnabled;
break;
case MONK:
m_pp.InnateSkills[InnateSurprise] = InnateEnabled;
m_pp.InnateSkills[InnateAwareness] = InnateEnabled;
default:
break;
}
}

View File

@ -199,6 +199,27 @@ struct RespawnOption
float heading;
};
// do not ask what all these mean because I have no idea!
// named from the client's CEverQuest::GetInnateDesc, they're missing some
enum eInnateSkill {
InnateEnabled = 0,
InnateAwareness = 1,
InnateBashDoor = 2,
InnateBreathFire = 3,
InnateHarmony = 4,
InnateInfravision = 6,
InnateLore = 8,
InnateNoBash = 9,
InnateRegen = 10,
InnateSlam = 11,
InnateSurprise = 12,
InnateUltraVision = 13,
InnateInspect = 14,
InnateOpen = 15,
InnateReveal = 16,
InnateSkillMax = 25, // size of array in client
InnateDisabled = 255
};
const uint32 POPUPID_UPDATE_SHOWSTATSWINDOW = 1000000;
@ -406,6 +427,16 @@ public:
const int32& SetMana(int32 amount);
int32 CalcManaRegenCap();
// guild pool regen shit. Sends a SpawnAppearance with a value that regens to value * 0.001
void EnableAreaHPRegen(int value);
void DisableAreaHPRegen();
void EnableAreaManaRegen(int value);
void DisableAreaManaRegen();
void EnableAreaEndRegen(int value);
void DisableAreaEndRegen();
void EnableAreaRegens(int value);
void DisableAreaRegens();
void ServerFilter(SetServerFilter_Struct* filter);
void BulkSendTraderInventory(uint32 char_id);
void SendSingleTraderItem(uint32 char_id, int uniqueid);
@ -540,7 +571,7 @@ public:
/*Endurance and such*/
void CalcMaxEndurance(); //This calculates the maximum endurance we can have
int32 CalcBaseEndurance(); //Calculates Base End
int32 CalcEnduranceRegen(); //Calculates endurance regen used in DoEnduranceRegen()
int32 CalcEnduranceRegen(bool bCombat = false); //Calculates endurance regen used in DoEnduranceRegen()
int32 GetEndurance() const {return current_endurance;} //This gets our current endurance
int32 GetMaxEndurance() const {return max_end;} //This gets our endurance from the last CalcMaxEndurance() call
int32 CalcEnduranceRegenCap();
@ -719,6 +750,7 @@ public:
void SendTradeskillDetails(uint32 recipe_id);
bool TradeskillExecute(DBTradeskillRecipe_Struct *spec);
void CheckIncreaseTradeskill(int16 bonusstat, int16 stat_modifier, float skillup_modifier, uint16 success_modifier, EQEmu::skills::SkillType tradeskill);
void InitInnates();
void GMKill();
inline bool IsMedding() const {return medding;}
@ -760,6 +792,9 @@ public:
void SummonHorse(uint16 spell_id);
void SetHorseId(uint16 horseid_in);
uint16 GetHorseId() const { return horseId; }
bool CanMedOnHorse();
bool CanFastRegen() const { return ooc_regen; }
void NPCSpawn(NPC *target_npc, const char *identifier, uint32 extra = 0);
@ -862,6 +897,7 @@ public:
void SetHunger(int32 in_hunger);
void SetThirst(int32 in_thirst);
void SetConsumption(int32 in_hunger, int32 in_thirst);
bool IsStarved() const { if (GetGM() || !RuleB(Character, EnableHungerPenalties)) return false; return m_pp.hunger_level == 0 || m_pp.thirst_level == 0; }
bool CheckTradeLoreConflict(Client* other);
bool CheckTradeNonDroppable();
@ -1345,8 +1381,8 @@ private:
int32 CalcCorrup();
int32 CalcMaxHP();
int32 CalcBaseHP();
int32 CalcHPRegen();
int32 CalcManaRegen();
int32 CalcHPRegen(bool bCombat = false);
int32 CalcManaRegen(bool bCombat = false);
int32 CalcBaseManaRegen();
uint32 GetClassHPFactor();
void DoHPRegen();
@ -1410,6 +1446,7 @@ private:
std::string BuyerWelcomeMessage;
bool AbilityTimer;
int Haste; //precalced value
uint32 tmSitting; // time stamp started sitting, used for HP regen bonus added on MAY 5, 2004
int32 max_end;
int32 current_endurance;
@ -1513,9 +1550,10 @@ private:
unsigned int AggroCount; // How many mobs are aggro on us.
unsigned int RestRegenHP;
unsigned int RestRegenMana;
unsigned int RestRegenEndurance;
bool ooc_regen;
float AreaHPRegen;
float AreaManaRegen;
float AreaEndRegen;
bool EngagedRaidTarget;
uint32 SavedRaidRestTimer;

View File

@ -22,6 +22,8 @@
#include "../common/rulesys.h"
#include "../common/spdat.h"
#include "../common/data_verification.h"
#include "client.h"
#include "mob.h"
@ -231,16 +233,81 @@ int32 Client::LevelRegen()
return hp;
}
int32 Client::CalcHPRegen()
int32 Client::CalcHPRegen(bool bCombat)
{
int32 regen = LevelRegen() + itembonuses.HPRegen + spellbonuses.HPRegen;
regen += aabonuses.HPRegen + GroupLeadershipAAHealthRegeneration();
int item_regen = itembonuses.HPRegen; // worn spells and +regen, already capped
item_regen += GetHeroicSTA() / 20;
item_regen += aabonuses.HPRegen;
int base = 0;
auto base_data = database.GetBaseData(GetLevel(), GetClass());
if (base_data)
base = static_cast<int>(base_data->hp_regen);
auto level = GetLevel();
bool skip_innate = false;
if (IsSitting()) {
if (level >= 50) {
base++;
if (level >= 65)
base++;
}
if ((Timer::GetCurrentTime() - tmSitting) > 60000) {
if (!IsAffectedByBuffByGlobalGroup(GlobalGroup::Lich)) {
auto tic_diff = std::min((Timer::GetCurrentTime() - tmSitting) / 60000, static_cast<uint32>(9));
if (tic_diff != 1) { // starts at 2 mins
int tic_bonus = tic_diff * 1.5 * base;
if (m_pp.InnateSkills[InnateRegen] != InnateDisabled)
tic_bonus = tic_bonus * 1.2;
base = tic_bonus;
skip_innate = true;
} else if (m_pp.InnateSkills[InnateRegen] == InnateDisabled) { // no innate regen gets first tick
int tic_bonus = base * 1.5;
base = tic_bonus;
}
}
}
}
if (!skip_innate && m_pp.InnateSkills[InnateRegen] != InnateDisabled) {
if (level >= 50) {
++base;
if (level >= 55)
++base;
}
base *= 2;
}
if (IsStarved())
base = 0;
base += GroupLeadershipAAHealthRegeneration();
// some IsKnockedOut that sets to -1
base = base * 100.0f * AreaHPRegen * 0.01f + 0.5f;
// another check for IsClient && !(base + item_regen) && Cur_HP <= 0 do --base; do later
if (!bCombat && CanFastRegen() && (IsSitting() || CanMedOnHorse())) {
auto fast_mod = RuleI(Character, RestRegenHP); // TODO: this is actually zone based
auto max_hp = GetMaxHP();
int fast_regen = 6 * (max_hp / fast_mod);
if (base < fast_regen) // weird, but what the client is doing
base = fast_regen;
}
int regen = base + item_regen + spellbonuses.HPRegen; // TODO: client does this in buff tick
return (regen * RuleI(Character, HPRegenMultiplier) / 100);
}
int32 Client::CalcHPRegenCap()
{
int cap = RuleI(Character, ItemHealthRegenCap) + itembonuses.HeroicSTA / 25;
int cap = RuleI(Character, ItemHealthRegenCap);
if (GetLevel() > 60)
cap = std::max(cap, GetLevel() - 30); // if the rule is set greater than normal I guess
if (GetLevel() > 65)
cap += GetLevel() - 65;
cap += aabonuses.ItemHPRegenCap + spellbonuses.ItemHPRegenCap + itembonuses.ItemHPRegenCap;
return (cap * RuleI(Character, HPRegenMultiplier) / 100);
}
@ -1169,43 +1236,72 @@ int32 Client::CalcBaseManaRegen()
return regen;
}
int32 Client::CalcManaRegen()
int32 Client::CalcManaRegen(bool bCombat)
{
uint8 clevel = GetLevel();
int32 regen = 0;
//this should be changed so we dont med while camping, etc...
if (IsSitting() || (GetHorseId() != 0)) {
BuffFadeBySitModifier();
if (HasSkill(EQEmu::skills::SkillMeditate)) {
this->medding = true;
regen = (((GetSkill(EQEmu::skills::SkillMeditate) / 10) + (clevel - (clevel / 4))) / 4) + 4;
regen += spellbonuses.ManaRegen + itembonuses.ManaRegen;
CheckIncreaseSkill(EQEmu::skills::SkillMeditate, nullptr, -5);
}
else {
regen = 2 + spellbonuses.ManaRegen + itembonuses.ManaRegen;
int regen = 0;
auto level = GetLevel();
if (!IsStarved()) {
// client does some base regen for shrouds here
if (IsSitting() || CanMedOnHorse()) {
// kind of weird to do it here w/e
// client does some base medding regen for shrouds here
if (GetClass() != BARD) {
auto skill = GetSkill(EQEmu::skills::SkillMeditate);
if (skill > 0) {
regen++;
if (skill > 1)
regen++;
if (skill >= 15)
regen += skill / 15;
}
}
}
}
else {
this->medding = false;
regen = 2 + spellbonuses.ManaRegen + itembonuses.ManaRegen;
if (level > 61) {
regen++;
if (level > 63)
regen++;
}
//AAs
regen += aabonuses.ManaRegen;
// add in + 1 bonus for SE_CompleteHeal, but we don't do anything for it yet?
int item_bonus = itembonuses.ManaRegen; // this is capped already
int heroic_bonus = 0;
switch (GetCasterClass()) {
case 'W':
heroic_bonus = GetHeroicWIS();
break;
default:
heroic_bonus = GetHeroicINT();
break;
}
item_bonus += heroic_bonus / 25;
regen += item_bonus;
if (level <= 70 && regen > 65)
regen = 65;
regen = regen * 100.0f * AreaManaRegen * 0.01f + 0.5f;
if (!bCombat && CanFastRegen() && (IsSitting() || CanMedOnHorse())) {
auto fast_mod = RuleI(Character, RestRegenMana); // TODO: this is actually zone based
auto max_mana = GetMaxMana();
int fast_regen = 6 * (max_mana / fast_mod);
if (regen < fast_regen) // weird, but what the client is doing
regen = fast_regen;
}
regen += spellbonuses.ManaRegen; // TODO: live does this in buff tick
return (regen * RuleI(Character, ManaRegenMultiplier) / 100);
}
int32 Client::CalcManaRegenCap()
{
int32 cap = RuleI(Character, ItemManaRegenCap) + aabonuses.ItemManaRegenCap;
switch (GetCasterClass()) {
case 'I':
cap += (itembonuses.HeroicINT / 25);
break;
case 'W':
cap += (itembonuses.HeroicWIS / 25);
break;
}
return (cap * RuleI(Character, ManaRegenMultiplier) / 100);
}
@ -2091,16 +2187,91 @@ int32 Client::CalcBaseEndurance()
return base_end;
}
int32 Client::CalcEnduranceRegen()
int32 Client::CalcEnduranceRegen(bool bCombat)
{
int32 regen = int32(GetLevel() * 4 / 10) + 2;
regen += aabonuses.EnduranceRegen + spellbonuses.EnduranceRegen + itembonuses.EnduranceRegen;
int base = 0;
if (!IsStarved()) {
auto base_data = database.GetBaseData(GetLevel(), GetClass());
if (base_data) {
base = static_cast<int>(base_data->end_regen);
if (!auto_attack && base > 0)
base += base / 2;
}
}
// so when we are mounted, our local client SpeedRun is always 0, so this is always false, but the packets we process it to our own shit :P
bool is_running = runmode && animation != 0 && GetHorseId() == 0; // TODO: animation is really what MQ2 calls SpeedRun
int weight_limit = GetSTR();
auto level = GetLevel();
if (GetClass() == MONK) {
if (level > 99)
weight_limit = 58;
else if (level > 94)
weight_limit = 57;
else if (level > 89)
weight_limit = 56;
else if (level > 84)
weight_limit = 55;
else if (level > 79)
weight_limit = 54;
else if (level > 64)
weight_limit = 53;
else if (level > 63)
weight_limit = 50;
else if (level > 61)
weight_limit = 47;
else if (level > 59)
weight_limit = 45;
else if (level > 54)
weight_limit = 40;
else if (level > 50)
weight_limit = 38;
else if (level > 44)
weight_limit = 36;
else if (level > 29)
weight_limit = 34;
else if (level > 14)
weight_limit = 32;
else
weight_limit = 30;
}
bool encumbered = (CalcCurrentWeight() / 10) >= weight_limit;
if (is_running)
base += level / -15;
if (encumbered)
base += level / -15;
auto item_bonus = GetHeroicAGI() + GetHeroicDEX() + GetHeroicSTA() + GetHeroicSTR();
item_bonus = item_bonus / 4 / 50;
item_bonus += itembonuses.EnduranceRegen; // this is capped already
base += item_bonus;
base = base * AreaEndRegen + 0.5f;
auto aa_regen = aabonuses.EnduranceRegen;
int regen = base;
if (!bCombat && CanFastRegen() && (IsSitting() || CanMedOnHorse())) {
auto fast_mod = RuleI(Character, RestRegenEnd); // TODO: this is actually zone based
auto max_end = GetMaxEndurance();
int fast_regen = 6 * (max_end / fast_mod);
if (aa_regen < fast_regen) // weird, but what the client is doing
aa_regen = fast_regen;
}
regen += aa_regen;
regen += spellbonuses.EnduranceRegen; // TODO: client does this in buff tick
return (regen * RuleI(Character, EnduranceRegenMultiplier) / 100);
}
int32 Client::CalcEnduranceRegenCap()
{
int cap = (RuleI(Character, ItemEnduranceRegenCap) + itembonuses.HeroicSTR / 25 + itembonuses.HeroicDEX / 25 + itembonuses.HeroicAGI / 25 + itembonuses.HeroicSTA / 25);
int cap = RuleI(Character, ItemEnduranceRegenCap);
return (cap * RuleI(Character, EnduranceRegenMultiplier) / 100);
}

View File

@ -1414,6 +1414,8 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app)
if (class_ == MONK)
consume_food_timer.SetTimer(CONSUMPTION_MNK_TIMER);
InitInnates();
/* If GM not set in DB, and does not meet min status to be GM, reset */
if (m_pp.gm && admin < minStatusToBeGM)
m_pp.gm = 0;
@ -13054,6 +13056,8 @@ void Client::Handle_OP_SpawnAppearance(const EQApplicationPacket *app)
InterruptSpell();
SetFeigned(false);
BindWound(this, false, true);
tmSitting = Timer::GetCurrentTime();
BuffFadeBySitModifier();
}
else if (sa->parameter == ANIM_CROUCH) {
if (!UseBardSpellLogic())

View File

@ -1820,7 +1820,7 @@ void Client::OPGMSummon(const EQApplicationPacket *app)
}
void Client::DoHPRegen() {
SetHP(GetHP() + CalcHPRegen() + RestRegenHP);
SetHP(GetHP() + CalcHPRegen());
SendHPUpdate();
}
@ -1828,7 +1828,10 @@ void Client::DoManaRegen() {
if (GetMana() >= max_mana && spellbonuses.ManaRegen >= 0)
return;
SetMana(GetMana() + CalcManaRegen() + RestRegenMana);
if (GetMana() < max_mana && (IsSitting() || CanMedOnHorse()) && HasSkill(EQEmu::skills::SkillMeditate))
CheckIncreaseSkill(EQEmu::skills::SkillMeditate, nullptr, -5);
SetMana(GetMana() + CalcManaRegen());
CheckManaEndUpdate();
}
@ -1869,10 +1872,11 @@ void Client::DoStaminaHungerUpdate()
void Client::DoEnduranceRegen()
{
if(GetEndurance() >= GetMaxEndurance())
return;
// endurance has some negative mods that could result in a negative regen when starved
int regen = CalcEnduranceRegen();
SetEndurance(GetEndurance() + CalcEnduranceRegen() + RestRegenEndurance);
if (regen < 0 || (regen > 0 && GetEndurance() < GetMaxEndurance()))
SetEndurance(GetEndurance() + regen);
}
void Client::DoEnduranceUpkeep() {
@ -1921,12 +1925,12 @@ void Client::CalcRestState() {
// The client must have been out of combat for RuleI(Character, RestRegenTimeToActivate) seconds,
// must be sitting down, and must not have any detrimental spells affecting them.
//
if(!RuleI(Character, RestRegenPercent))
if(!RuleB(Character, RestRegenEnabled))
return;
RestRegenHP = RestRegenMana = RestRegenEndurance = 0;
ooc_regen = false;
if(AggroCount || !IsSitting())
if(AggroCount || !(IsSitting() || CanMedOnHorse()))
return;
if(!rest_timer.Check(false))
@ -1941,12 +1945,8 @@ void Client::CalcRestState() {
}
}
RestRegenHP = (GetMaxHP() * RuleI(Character, RestRegenPercent) / 100);
ooc_regen = true;
RestRegenMana = (GetMaxMana() * RuleI(Character, RestRegenPercent) / 100);
if(RuleB(Character, RestRegenEndurance))
RestRegenEndurance = (GetMaxEndurance() * RuleI(Character, RestRegenPercent) / 100);
}
void Client::DoTracking()

View File

@ -151,6 +151,7 @@ void Client::SummonHorse(uint16 spell_id) {
uint16 tmpID = horse->GetID();
SetHorseId(tmpID);
BuffFadeBySitModifier();
}

View File

@ -1154,7 +1154,7 @@ void Merc::CalcRestState() {
// The bot must have been out of combat for RuleI(Character, RestRegenTimeToActivate) seconds,
// must be sitting down, and must not have any detrimental spells affecting them.
//
if(!RuleI(Character, RestRegenPercent))
if(!RuleB(Character, RestRegenEnabled))
return;
RestRegenHP = RestRegenMana = RestRegenEndurance = 0;
@ -1174,12 +1174,11 @@ void Merc::CalcRestState() {
}
}
RestRegenHP = (GetMaxHP() * RuleI(Character, RestRegenPercent) / 100);
RestRegenHP = 6 * (GetMaxHP() / RuleI(Character, RestRegenHP));
RestRegenMana = (GetMaxMana() * RuleI(Character, RestRegenPercent) / 100);
RestRegenMana = 6 * (GetMaxMana() / RuleI(Character, RestRegenMana));
if(RuleB(Character, RestRegenEndurance))
RestRegenEndurance = (GetMaxEndurance() * RuleI(Character, RestRegenPercent) / 100);
RestRegenEndurance = 6 * (GetMaxEndurance() / RuleI(Character, RestRegenEnd));
}
bool Merc::HasSkill(EQEmu::skills::SkillType skill_id) const {

View File

@ -336,6 +336,7 @@ public:
void BuffFadeDetrimentalByCaster(Mob *caster);
void BuffFadeBySitModifier();
bool IsAffectedByBuff(uint16 spell_id);
bool IsAffectedByBuffByGlobalGroup(GlobalGroup group);
void BuffModifyDurationBySpellID(uint16 spell_id, int32 newDuration);
int AddBuff(Mob *caster, const uint16 spell_id, int duration = 0, int32 level_override = -1);
int CanBuffStack(uint16 spellid, uint8 caster_level, bool iFailIfOverwrite = false);

View File

@ -60,6 +60,9 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove
const SPDat_Spell_Struct &spell = spells[spell_id];
if (spell.disallow_sit && IsBuffSpell(spell_id) && IsClient() && (CastToClient()->IsSitting() || CastToClient()->GetHorseId() != 0))
return false;
bool c_override = false;
if (caster && caster->IsClient() && GetCastedSpellInvSlot() > 0) {
const EQEmu::ItemInstance *inst = caster->CastToClient()->GetInv().GetItem(GetCastedSpellInvSlot());

View File

@ -4234,6 +4234,19 @@ bool Mob::IsAffectedByBuff(uint16 spell_id)
return false;
}
bool Mob::IsAffectedByBuffByGlobalGroup(GlobalGroup group)
{
int buff_count = GetMaxTotalSlots();
for (int i = 0; i < buff_count; ++i) {
if (buffs[i].spellid == SPELL_UNKNOWN)
continue;
if (spells[buffs[i].spellid].spell_category == static_cast<int>(group))
return true;
}
return false;
}
// checks if 'this' can be affected by spell_id from caster
// returns true if the spell should fail, false otherwise
bool Mob::IsImmuneToSpell(uint16 spell_id, Mob *caster)